Skip to content

Commit 4c164f5

Browse files
committed
v1.0.2: Fix bring-to-front functionality
- Implement two-phase window positioning and activation - Use NSRunningApplication.activate() for proper app activation - Bring windows to front in reverse order (last window becomes frontmost) - Remove layout loading delays for instant performance - Fix browser staying behind Cursor when loading layouts Changes: - LayoutManager: Restructured loadLayout to position all windows first, then activate in reverse order - Enhanced debugging with positioning and activation logs - Maintained fast layout loading without delays
1 parent 6f7449f commit 4c164f5

File tree

4 files changed

+68
-52
lines changed

4 files changed

+68
-52
lines changed
-812 Bytes
Binary file not shown.

snap/AppKitMenuManager.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import AppKit
22
import SwiftUI
33
import Combine
44
import ServiceManagement
5+
import Carbon
6+
import CoreGraphics
57

68
class CustomMenu: NSMenu {
79
override func performActionForItem(at index: Int) {
@@ -20,7 +22,7 @@ class AppKitMenuManager: NSObject, ObservableObject, NSMenuDelegate {
2022
private var menu: NSMenu?
2123
private let layoutManager = LayoutManager.shared
2224

23-
// Global hotkey monitoring
25+
// Global hotkey monitoring - using NSEvent with proper priority
2426
private var registeredShortcuts: [String: (layoutName: String, eventMonitor: Any)] = [:]
2527

2628
override init() {
@@ -770,18 +772,22 @@ class AppKitMenuManager: NSObject, ObservableObject, NSMenuDelegate {
770772
NSEvent.removeMonitor(eventMonitor)
771773
}
772774
registeredShortcuts.removeAll()
775+
print("🔄 Cleared all registered shortcuts")
773776
}
774777

775778
private func registerGlobalShortcut(_ shortcutString: String, for layoutName: String) {
776779
guard let (keyCode, modifiers) = parseShortcutString(shortcutString) else {
780+
print("❌ Failed to parse shortcut '\(shortcutString)'")
777781
return
778782
}
779783

780784
// Check for duplicate shortcuts
781785
if registeredShortcuts[shortcutString] != nil {
786+
print("⚠️ Shortcut '\(shortcutString)' already registered")
782787
return
783788
}
784789

790+
// Use NSEvent with aggressive monitoring
785791
let eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in
786792
// Check if the key matches
787793
let keyMatches = event.keyCode == keyCode
@@ -798,6 +804,9 @@ class AppKitMenuManager: NSObject, ObservableObject, NSMenuDelegate {
798804
let modifiersMatch = actualRelevantModifiers == expectedRelevantModifiers
799805

800806
if keyMatches && modifiersMatch {
807+
print("🎯 Intercepted shortcut '\(shortcutString)' - loading layout '\(layoutName)'")
808+
809+
// Execute immediately on main thread
801810
DispatchQueue.main.async {
802811
Task { await self?.layoutManager.loadLayout(name: layoutName) }
803812
}
@@ -806,7 +815,11 @@ class AppKitMenuManager: NSObject, ObservableObject, NSMenuDelegate {
806815

807816
if let monitor = eventMonitor {
808817
registeredShortcuts[shortcutString] = (layoutName: layoutName, eventMonitor: monitor)
818+
print("✅ Successfully registered shortcut '\(shortcutString)' for layout '\(layoutName)'")
819+
print("📋 KeyCode: \(keyCode), Modifiers: \(modifiers)")
820+
print("🎯 Monitoring global events - will intercept when pressed")
809821
} else {
822+
print("❌ Failed to register shortcut '\(shortcutString)'")
810823
}
811824
}
812825

@@ -914,6 +927,7 @@ class AppKitMenuManager: NSObject, ObservableObject, NSMenuDelegate {
914927
default: return nil
915928
}
916929
}
930+
917931
}
918932

919933
// MARK: - Key Capture View

snap/LayoutManager.swift

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ class LayoutManager: ObservableObject {
160160
}
161161

162162
// Use Accessibility API for window manipulation - more reliable than AppleScript
163+
// First, collect all windows that need to be positioned
164+
var windowsToPosition: [(windowElement: AXUIElement, app: NSRunningApplication, saved: [String: Any], targetWindowTitle: String)] = []
165+
163166
for (index, saved) in filteredLayouts.enumerated() {
164167
guard let savedOwner = saved["owner"] as? String,
165168
let savedName = saved["name"] as? String,
@@ -179,9 +182,7 @@ class LayoutManager: ObservableObject {
179182
if app == nil, let bundleId = saved["bundleId"] as? String, !bundleId.isEmpty,
180183
let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) {
181184
try? await NSWorkspace.shared.openApplication(at: url, configuration: NSWorkspace.OpenConfiguration())
182-
// Wait a bit for the app to launch
183-
try? await Task.sleep(nanoseconds: 5_000_000_000)
184-
// Refresh running apps
185+
// Refresh running apps immediately
185186
let updatedApps = NSWorkspace.shared.runningApplications
186187
app = updatedApps.first(where: { $0.localizedName == savedOwner })
187188
}
@@ -263,58 +264,59 @@ class LayoutManager: ObservableObject {
263264
}
264265
}
265266

266-
// Apply the layout to the target window
267+
// Collect window for positioning
267268
if let windowElement = targetWindowElement {
268-
// Get current window position and size to check if it needs to be moved
269-
var currentPosition: CFTypeRef?
270-
var currentSize: CFTypeRef?
271-
AXUIElementCopyAttributeValue(windowElement, kAXPositionAttribute as CFString, &currentPosition)
272-
AXUIElementCopyAttributeValue(windowElement, kAXSizeAttribute as CFString, &currentSize)
273-
274-
var currentPos = CGPoint.zero
275-
var currentSz = CGSize.zero
276-
277-
if let pos = currentPosition {
278-
AXValueGetValue(pos as! AXValue, .cgPoint, &currentPos)
279-
}
280-
if let sz = currentSize {
281-
AXValueGetValue(sz as! AXValue, .cgSize, &currentSz)
282-
}
283-
284-
let targetPosition = CGPoint(x: x, y: y)
285-
let targetSize = CGSize(width: width, height: height)
286-
287-
// Always restore windows to their exact saved positions when loading a layout
288-
// This ensures that manually resized windows get restored to their saved layout
289-
// No tolerance check - always apply the saved layout
290-
291-
// Set position
292-
var position = targetPosition
293-
let posValue = AXValueCreate(.cgPoint, &position)
294-
let posResult = AXUIElementSetAttributeValue(windowElement, kAXPositionAttribute as CFString, posValue!)
295-
if posResult != AXError.success {
296-
continue
297-
}
298-
299-
// Set size
300-
var size = targetSize
301-
let sizeValue = AXValueCreate(.cgSize, &size)
302-
let sizeResult = AXUIElementSetAttributeValue(windowElement, kAXSizeAttribute as CFString, sizeValue!)
303-
if sizeResult != AXError.success {
304-
continue
305-
}
306-
307-
// Bring window to front
308-
AXUIElementSetAttributeValue(windowElement, kAXFrontmostAttribute as CFString, kCFBooleanTrue)
309-
269+
windowsToPosition.append((windowElement: windowElement, app: app, saved: saved, targetWindowTitle: targetWindowTitle))
310270
windowFound = true
311271
}
272+
}
273+
274+
// Now position all windows first
275+
for (windowElement, app, saved, targetWindowTitle) in windowsToPosition {
276+
guard let bounds = saved["bounds"] as? [String: Any],
277+
let x = bounds["X"] as? Double,
278+
let y = bounds["Y"] as? Double,
279+
let width = bounds["Width"] as? Double,
280+
let height = bounds["Height"] as? Double else {
281+
continue
282+
}
312283

313-
// Add a small delay between window operations to ensure system stability
314-
// This helps prevent race conditions when multiple windows are being repositioned
315-
if index < filteredLayouts.count - 1 {
316-
try? await Task.sleep(nanoseconds: 100_000_000) // 100ms delay
284+
let targetPosition = CGPoint(x: x, y: y)
285+
let targetSize = CGSize(width: width, height: height)
286+
287+
// Set position
288+
var position = targetPosition
289+
let posValue = AXValueCreate(.cgPoint, &position)
290+
let posResult = AXUIElementSetAttributeValue(windowElement, kAXPositionAttribute as CFString, posValue!)
291+
if posResult != AXError.success {
292+
continue
293+
}
294+
295+
// Set size
296+
var size = targetSize
297+
let sizeValue = AXValueCreate(.cgSize, &size)
298+
let sizeResult = AXUIElementSetAttributeValue(windowElement, kAXSizeAttribute as CFString, sizeValue!)
299+
if sizeResult != AXError.success {
300+
continue
317301
}
302+
303+
print("📐 Positioned window '\(targetWindowTitle)' at (\(x), \(y)) with size \(width)x\(height)")
304+
}
305+
306+
// Now bring windows to front in reverse order (last window becomes frontmost)
307+
for (windowElement, app, saved, targetWindowTitle) in windowsToPosition.reversed() {
308+
guard let savedOwner = saved["owner"] as? String else { continue }
309+
310+
// Method 1: Activate the application first
311+
app.activate(options: [.activateIgnoringOtherApps])
312+
313+
// Method 2: Use Accessibility API to bring window to front
314+
AXUIElementSetAttributeValue(windowElement, kAXFrontmostAttribute as CFString, kCFBooleanTrue)
315+
316+
// Method 3: Set as main window
317+
AXUIElementSetAttributeValue(windowElement, kAXMainAttribute as CFString, kCFBooleanTrue)
318+
319+
print("🎯 Brought window '\(targetWindowTitle)' to front for app '\(savedOwner)'")
318320
}
319321
}
320322

snap/MenuBarContent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ struct MenuBarContent: View {
142142
StyledMenuLabel(name: name, shortcut: shortcut)
143143
}
144144
}
145-
}
145+
}
146146
}
147147

148148
Divider()

0 commit comments

Comments
 (0)