Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@
buildConfigurationList = A8E59C44297F5E9B0064D4BA /* Build configuration list for PBXNativeTarget "Loop" */;
buildPhases = (
A8E59C31297F5E9A0064D4BA /* Sources */,
A80FD0D82CB34BA300DCC00B /* Run SwiftFormat */,
A8E59C32297F5E9A0064D4BA /* Frameworks */,
A8E59C33297F5E9A0064D4BA /* Resources */,
);
Expand Down Expand Up @@ -175,28 +174,6 @@
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
A80FD0D82CB34BA300DCC00B /* Run SwiftFormat */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run SwiftFormat";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Default Homebrew installation path on Intel-based Macs\nHOMEBREW_INTEL_PATH=\"/usr/local/bin/swiftformat\"\n\n# Default Homebrew installation path on Apple Silicon Macs\nHOMEBREW_ARM_PATH=\"/opt/homebrew/bin/swiftformat\"\n\n# Determine the architecture of the machine (arm64 or x86_64)\nARCH=$(uname -m)\n\n# Set the Homebrew path based on the architecture\nif [ \"$ARCH\" = \"arm64\" ]; then\n SWIFTFORMAT_PATH=\"$HOMEBREW_ARM_PATH\"\nelse\n SWIFTFORMAT_PATH=\"$HOMEBREW_INTEL_PATH\"\nfi\n\n# Check if SwiftFormat is installed via Homebrew\nif [ -x \"$SWIFTFORMAT_PATH\" ]; then\n \"$SWIFTFORMAT_PATH\" .\nelse\n echo \"warning: SwiftFormat not installed via Homebrew or not found in expected paths\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
A8E59C31297F5E9A0064D4BA /* Sources */ = {
isa = PBXSourcesBuildPhase;
Expand Down
26 changes: 20 additions & 6 deletions Loop/App/DataPatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import Scribe

enum DataPatcher {
static func run() {
let initialPatches = Defaults[.patchesApplied]
let initialPatches: Patches = Defaults[.patchesApplied]

if !initialPatches.contains(.accentColorMode) {
runPatch(patch: .changeToAccentColorMode, initial: initialPatches) {
// Migrate to accent color mode
// We need to migrate `useSystemAccentColor` and `processWallpaper` over to `accentColorMode`
let useSystemAccentColor: Bool = Defaults[.useSystemAccentColor]
Expand All @@ -27,14 +27,28 @@ enum DataPatcher {
Defaults[.accentColorMode] = .custom
}

Defaults[.patchesApplied].formUnion(.accentColorMode)
Log.info("Ran patch accentColorMode", category: .dataPatcher)
Defaults.reset(.useSystemAccentColor)
Defaults.reset(.processWallpaper)
}

runPatch(patch: .removeRevealedStashedWindows, initial: initialPatches) {
Defaults.reset(.stashManagerRevealedWindows)
}
}

private static func runPatch(patch: Patches, initial: Patches, with callback: () -> ()) {
if !initial.contains(patch) {
callback()

Defaults[.patchesApplied].formUnion(patch)
Log.info("Ran patch \(patch)", category: .dataPatcher)
}
}

struct Patch: OptionSet, Defaults.Serializable {
struct Patches: OptionSet, Defaults.Serializable {
let rawValue: Int

static let accentColorMode = Self(rawValue: 1 << 0)
static let changeToAccentColorMode = Self(rawValue: 1 << 0)
static let removeRevealedStashedWindows = Self(rawValue: 1 << 1)
}
}
17 changes: 6 additions & 11 deletions Loop/Core/LoopManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ final class LoopManager: ObservableObject {
// MARK: - Opening/Closing Loop

extension LoopManager {
private func openLoop(startingAction: WindowAction?) {
private func openLoop(startingAction: WindowAction) {
guard AccessibilityManager.shared.isGranted else {
return
}
Expand All @@ -96,9 +96,7 @@ extension LoopManager {
/// In these cases, we can simply update the action instead of reopening the Loop.
/// Enabling keybindObserver was considered as a workaround, but it doesn't start quickly enough.
/// Although Karabiner-Elements sends key events separately, they arrive in quick succession.
if let startingAction {
changeAction(startingAction, disableHapticFeedback: true)
}
changeAction(startingAction, disableHapticFeedback: true)
return
}

Expand All @@ -111,7 +109,7 @@ extension LoopManager {
return
}

Log.info("Opening Loop with starting action: \(startingAction?.description ?? "(none)") and target window: \(window?.description ?? "(none)")", category: .loopManager)
Log.info("Opening Loop with starting action: \(startingAction.description) and target window: \(window?.description ?? "(none)")", category: .loopManager)

// Record the first frame in advance if the preview window is disabled
if let window,
Expand Down Expand Up @@ -152,10 +150,7 @@ extension LoopManager {
}

isLoopActive = true

if let startingAction {
changeAction(startingAction, disableHapticFeedback: true)
}
changeAction(startingAction, disableHapticFeedback: true)
}

private func closeLoop(forceClose: Bool) {
Expand Down Expand Up @@ -196,7 +191,7 @@ extension LoopManager {
LoopManager.lastTargetFrame = .zero
}

private func openWindows(startingAction: WindowAction?, window: Window?) {
private func openWindows(startingAction: WindowAction, window: Window?) {
if Defaults[.previewVisibility], let screenToResizeOn {
previewController.open(
screen: screenToResizeOn,
Expand Down Expand Up @@ -236,7 +231,7 @@ extension LoopManager {
canAdvanceCycle: Bool = true
) {
guard
!currentAction.isSameManipulation(as: newAction) || newAction.shouldImmediatelyExecuteAction,
currentAction.id != newAction.id || newAction.shouldImmediatelyExecuteAction,
isLoopActive,
let currentScreen = screenToResizeOn
else {
Expand Down
6 changes: 3 additions & 3 deletions Loop/Core/Observers/Helpers/DoubleClickTimer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ import Defaults
/// two occur within the system-defined (and user-customizable) `NSEvent.doubleClickInterval`.
final class DoubleClickTimer {
private var lastTriggerKeyPressTime: Date?
private let openCallback: (WindowAction?) -> ()
private let openCallback: (WindowAction) -> ()
private var doubleClickInterval: TimeInterval { NSEvent.doubleClickInterval }

/// Creates a new `DoubleClickTimer` instance with the specified callback to invoke on a double-press event.
/// - Parameter openCallback: A closure that is called when a double-click is detected. The closure receives the `WindowAction` associated with the trigger as its parameter.
init(openCallback: @escaping (WindowAction?) -> ()) {
init(openCallback: @escaping (WindowAction) -> ()) {
self.openCallback = openCallback
}

/// Handles a trigger event (such as a key press) and determines whether it qualifies as a "double-click".
/// - Parameter startingAction: The `WindowAction` associated with the trigger.
func handleTrigger(startingAction: WindowAction?) {
func handleTrigger(startingAction: WindowAction) {
let now = Date()

// If we detect a double-press, trigger immediately. Otherwise, just record the time
Expand Down
12 changes: 6 additions & 6 deletions Loop/Core/Observers/Helpers/TriggerDelayTimer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ import Foundation
/// In that case, use the `updateStartingAction` method.
final class TriggerDelayTimer {
private var triggerDelayTimer: Task<(), Never>?
private var startingAction: WindowAction?
private let openCallback: (WindowAction?) -> ()
private var startingAction: WindowAction = .init(.noSelection)
private let openCallback: (WindowAction) -> ()
private var triggerDelay: CGFloat { Defaults[.triggerDelay] }

/// Indicates whether the delay timer is currently active.
var isActive: Bool { triggerDelayTimer != nil }

/// Creates a new `TriggerDelayTimer` instance with the specified callback to invoke after a user-configured delay has elapsed.
/// - Parameter openCallback: A closure that is called once the trigger delay completes successfully. The closure receives the latest `WindowAction` depending on what has been set.
init(openCallback: @escaping (WindowAction?) -> ()) {
init(openCallback: @escaping (WindowAction) -> ()) {
self.openCallback = openCallback
}

Expand All @@ -38,7 +38,7 @@ final class TriggerDelayTimer {
/// If another trigger is received before the delay elapses, the previous timer is canceled and restarted.
/// Once the configured delay duration passes without interruption, the provided callback is invoked, with the latest inputted starting action.
/// - Parameter startingAction: The `WindowAction` associated with the trigger.
func handleTrigger(startingAction action: WindowAction?) {
func handleTrigger(startingAction action: WindowAction) {
startingAction = action
cancel() // Ensure no previous timer is active

Expand All @@ -53,14 +53,14 @@ final class TriggerDelayTimer {

/// Updates the stored `startingAction` value without restarting the timer. To be used with keybinds.
/// - Parameter newAction: The new `WindowAction` to associate with the current trigger delay.
func updateStartingAction(with newAction: WindowAction?) {
func updateStartingAction(with newAction: WindowAction) {
startingAction = newAction
}

/// Cancels any active delay timer and clears the stored action.
func cancel() {
triggerDelayTimer?.cancel()
triggerDelayTimer = nil
startingAction = nil
startingAction = .init(.noSelection)
}
}
13 changes: 8 additions & 5 deletions Loop/Core/Observers/KeybindTrigger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Defaults
final class KeybindTrigger {
// Parameters
private let windowActionCache: WindowActionCache
private let openCallback: (WindowAction?) -> ()
private let openCallback: (WindowAction) -> ()
private let closeCallback: (Bool) -> ()
private let checkIfLoopOpen: () -> Bool

Expand Down Expand Up @@ -58,7 +58,7 @@ final class KeybindTrigger {
/// - closeCallback: what to do when the trigger key is released, and Loop should be closed.
init(
windowActionCache: WindowActionCache,
openCallback: @escaping (WindowAction?) -> (),
openCallback: @escaping (WindowAction) -> (),
closeCallback: @escaping (Bool) -> (),
checkIfLoopOpen: @escaping () -> Bool
) {
Expand Down Expand Up @@ -193,7 +193,10 @@ final class KeybindTrigger {

// Only trigger Loop without an action if the only pressed keys perfectly matches the trigger key.
if allPressedKeys == triggerKey {
openLoop(startingAction: nil, overrideExistingTriggerDelayTimerAction: !isARepeat)
openLoop(
startingAction: .init(.noSelection),
overrideExistingTriggerDelayTimerAction: !isARepeat
)
return .opening
}
} else {
Expand All @@ -205,7 +208,7 @@ final class KeybindTrigger {
return .forward
}

private func openLoop(startingAction: WindowAction?, overrideExistingTriggerDelayTimerAction: Bool) {
private func openLoop(startingAction: WindowAction, overrideExistingTriggerDelayTimerAction: Bool) {
if checkIfLoopOpen() {
openCallback(startingAction) // Only update Loop to the latest WindowAction
} else {
Expand All @@ -230,7 +233,7 @@ final class KeybindTrigger {
}

private func startTriggerDelayTimer(
startingAction: WindowAction?,
startingAction: WindowAction,
overrideExistingTriggerDelayTimerAction: Bool
) {
// If a trigger delay timer is already active, only update its startingAction when
Expand Down
12 changes: 6 additions & 6 deletions Loop/Core/Observers/MiddleClickTrigger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Defaults
/// Reads middle-click events using a PassiveEventMonitor, and triggers Loop open/close callbacks, when appropriate.
final class MiddleClickTrigger {
// Callbacks
private let openCallback: (WindowAction?) -> ()
private let openCallback: (WindowAction) -> ()
private let closeCallback: (Bool) -> ()

// State-tracking
Expand All @@ -27,7 +27,7 @@ final class MiddleClickTrigger {
guard let self else { return }

if useTriggerDelay {
triggerDelayTimer.handleTrigger(startingAction: nil)
triggerDelayTimer.handleTrigger(startingAction: .init(.noSelection))
} else {
openCallback(action)
}
Expand All @@ -38,7 +38,7 @@ final class MiddleClickTrigger {
/// - openCallback: what to do when the middle mouse button is pressed, and Loop should be activated.
/// - closeCallback: what to do when the middle mouse button is released, and Loop should be closed.
init(
openCallback: @escaping (WindowAction?) -> (),
openCallback: @escaping (WindowAction) -> (),
closeCallback: @escaping (Bool) -> ()
) {
// We will never start off with an action from this trigger, so pass in nil
Expand Down Expand Up @@ -76,11 +76,11 @@ final class MiddleClickTrigger {
if event.type == .otherMouseDown,
event.getIntegerValueField(.mouseEventButtonNumber) == 2 {
if doubleClickToTrigger {
doubleClickTimer.handleTrigger(startingAction: nil)
doubleClickTimer.handleTrigger(startingAction: .init(.noSelection))
} else if useTriggerDelay {
triggerDelayTimer.handleTrigger(startingAction: nil)
triggerDelayTimer.handleTrigger(startingAction: .init(.noSelection))
} else {
openCallback(nil)
openCallback(.init(.noSelection))
}
} else {
triggerDelayTimer.cancel()
Expand Down
24 changes: 15 additions & 9 deletions Loop/Core/Observers/MouseInteractionObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ final class MouseInteractionObserver {
private var previousAngleToMouse: Angle = .zero
private var previousDistanceToMouse: CGFloat = .zero

private var radialMenuActions: [RadialMenuWindowAction] {
Defaults[.radialMenuActions]
private var radialMenuActions: [RadialMenuAction] {
RadialMenuAction.userConfiguredActions
}

private static let failedToResolveKeybindAction: WindowAction = .init(.noAction) // This helps to keep a stable ID

init(
windowActionCache: WindowActionCache,
changeAction: @escaping (WindowAction) -> (),
Expand Down Expand Up @@ -86,7 +88,7 @@ final class MouseInteractionObserver {
let initialMousePosition = getInitialMousePosition()
let currentMousePosition = NSEvent.mouseLocation

let angleToMouse = Angle(radians: initialMousePosition.angle(to: currentMousePosition))
let angleToMouse = initialMousePosition.angle(to: currentMousePosition) + .radians(.pi / 2)
let distanceToMouse = initialMousePosition.distance(to: currentMousePosition)

// Return if the mouse didn't move
Expand All @@ -101,7 +103,7 @@ final class MouseInteractionObserver {
previousAngleToMouse = angleToMouse
previousDistanceToMouse = distanceToMouse

var newAction: RadialMenuWindowAction? = nil
var newAction: RadialMenuAction? = nil

// If mouse over 50 points away, select half or quarter positions
if distanceToMouse > 50 - Defaults[.radialMenuThickness] {
Expand All @@ -110,23 +112,27 @@ final class MouseInteractionObserver {
return
}

let actions = Array(radialMenuActions[1...])
let actions = radialMenuActions.dropLast()
let actionAngleSpan = 360.0 / CGFloat(actions.count)
let halfAngleSpan = actionAngleSpan / 2.0
let index = Int((angleToMouse.normalized().degrees + halfAngleSpan) / actionAngleSpan) % actions.count
newAction = actions[index]
} else if distanceToMouse > noActionDistance {
newAction = radialMenuActions.first
newAction = radialMenuActions.last
}

Task { @MainActor in
switch newAction {
switch newAction?.type {
case let .custom(windowAction):
changeAction(windowAction)
case let .keybindReference(id):
if let action = windowActionCache.actionsByIdentifier[id] { changeAction(action) }
if let action = windowActionCache.actionsByIdentifier[id] {
changeAction(action)
} else {
changeAction(Self.failedToResolveKeybindAction)
}
case nil:
changeAction(.init(.noAction))
changeAction(.init(.noSelection))
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions Loop/Extensions/CGGeometry+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ extension CGFloat {
}

extension CGPoint {
func angle(to comparisonPoint: CGPoint) -> CGFloat {
func angle(to comparisonPoint: CGPoint) -> Angle {
let originX = comparisonPoint.x - x
let originY = comparisonPoint.y - y
let bearingRadians = -atan2f(Float(originY), Float(originX))

return CGFloat(bearingRadians)
return .radians(Double(bearingRadians))
}

func distance(to comparisonPoint: CGPoint) -> CGFloat {
Expand Down
Loading