@lattices/cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -9
- package/app/Info.plist +30 -0
- package/app/Lattices.app/Contents/Info.plist +8 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
- package/app/Lattices.entitlements +15 -0
- package/app/Package.swift +8 -1
- package/app/Resources/tap.wav +0 -0
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +45 -12
- package/app/Sources/AppShellView.swift +81 -8
- package/app/Sources/AudioProvider.swift +386 -0
- package/app/Sources/CheatSheetHUD.swift +261 -19
- package/app/Sources/DaemonProtocol.swift +13 -0
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DesktopModel.swift +189 -6
- package/app/Sources/DesktopModelTypes.swift +2 -0
- package/app/Sources/DiagnosticLog.swift +104 -2
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HUDBottomBar.swift +279 -0
- package/app/Sources/HUDController.swift +1158 -0
- package/app/Sources/HUDLeftBar.swift +849 -0
- package/app/Sources/HUDMinimap.swift +179 -0
- package/app/Sources/HUDRightBar.swift +774 -0
- package/app/Sources/HUDState.swift +367 -0
- package/app/Sources/HUDTopBar.swift +243 -0
- package/app/Sources/HandsOffSession.swift +802 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +49 -9
- package/app/Sources/IntentEngine.swift +962 -0
- package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
- package/app/Sources/Intents/DistributeIntent.swift +56 -0
- package/app/Sources/Intents/FocusIntent.swift +69 -0
- package/app/Sources/Intents/HelpIntent.swift +41 -0
- package/app/Sources/Intents/KillIntent.swift +47 -0
- package/app/Sources/Intents/LatticeIntent.swift +78 -0
- package/app/Sources/Intents/LaunchIntent.swift +67 -0
- package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
- package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
- package/app/Sources/Intents/ScanIntent.swift +52 -0
- package/app/Sources/Intents/SearchIntent.swift +190 -0
- package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
- package/app/Sources/Intents/TileIntent.swift +61 -0
- package/app/Sources/LatticesApi.swift +1275 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- package/app/Sources/MouseFinder.swift +222 -0
- package/app/Sources/OcrModel.swift +34 -1
- package/app/Sources/OmniSearchState.swift +99 -102
- package/app/Sources/OnboardingView.swift +457 -0
- package/app/Sources/PermissionChecker.swift +2 -12
- package/app/Sources/PiChatDock.swift +454 -0
- package/app/Sources/PiChatSession.swift +815 -0
- package/app/Sources/PiWorkspaceView.swift +364 -0
- package/app/Sources/PlacementSpec.swift +195 -0
- package/app/Sources/Preferences.swift +59 -0
- package/app/Sources/ProjectScanner.swift +58 -45
- package/app/Sources/ScreenMapState.swift +701 -55
- package/app/Sources/ScreenMapView.swift +843 -103
- package/app/Sources/ScreenMapWindowController.swift +22 -0
- package/app/Sources/SessionLayerStore.swift +285 -0
- package/app/Sources/SessionManager.swift +4 -1
- package/app/Sources/SettingsView.swift +186 -3
- package/app/Sources/Theme.swift +9 -8
- package/app/Sources/TmuxModel.swift +7 -0
- package/app/Sources/TmuxQuery.swift +27 -3
- package/app/Sources/VoiceChatView.swift +192 -0
- package/app/Sources/VoiceCommandWindow.swift +1594 -0
- package/app/Sources/VoiceIntentResolver.swift +671 -0
- package/app/Sources/VoxClient.swift +454 -0
- package/app/Sources/WindowTiler.swift +348 -87
- package/app/Sources/WorkspaceManager.swift +127 -18
- package/app/Tests/StageDragTests.swift +333 -0
- package/app/Tests/StageJoinTests.swift +313 -0
- package/app/Tests/StageManagerTests.swift +280 -0
- package/app/Tests/StageTileTests.swift +353 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/client.ts +16 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +280 -0
- package/bin/handsoff-worker.ts +740 -0
- package/bin/lattices-app.ts +338 -0
- package/bin/lattices-dev +208 -0
- package/bin/{lattices.js → lattices.ts} +777 -140
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +142 -0
- package/docs/api.md +153 -34
- package/docs/app.md +29 -1
- package/docs/config.md +5 -1
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/layers.md +20 -20
- package/docs/ocr.md +14 -5
- package/docs/overview.md +5 -1
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +374 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/tiling-reference.md +167 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice.md +219 -0
- package/package.json +29 -11
- package/bin/client.js +0 -4
- package/bin/lattices-app.js +0 -221
|
@@ -10,6 +10,85 @@ struct DisplayGeometry {
|
|
|
10
10
|
let label: String // e.g. "Built-in Retina Display", "LG UltraFine"
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// MARK: - Canvas Region
|
|
14
|
+
|
|
15
|
+
struct ScreenMapCanvasRegion: Identifiable {
|
|
16
|
+
enum Kind {
|
|
17
|
+
case overview
|
|
18
|
+
case display
|
|
19
|
+
case layer
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let id: String
|
|
23
|
+
let kind: Kind
|
|
24
|
+
let title: String
|
|
25
|
+
let subtitle: String
|
|
26
|
+
let rect: CGRect
|
|
27
|
+
let count: Int
|
|
28
|
+
let displayIndex: Int?
|
|
29
|
+
let layer: Int?
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
struct ScreenMapCanvasNavigationTarget {
|
|
33
|
+
let center: CGPoint
|
|
34
|
+
let rect: CGRect?
|
|
35
|
+
let zoomToFit: Bool
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
enum ScreenMapViewportPreset: String, CaseIterable, Identifiable {
|
|
39
|
+
case overview
|
|
40
|
+
case main
|
|
41
|
+
case topRight
|
|
42
|
+
case bottomLeft
|
|
43
|
+
case bottomRight
|
|
44
|
+
|
|
45
|
+
var id: String { rawValue }
|
|
46
|
+
|
|
47
|
+
var title: String {
|
|
48
|
+
switch self {
|
|
49
|
+
case .overview: return "All"
|
|
50
|
+
case .main: return "Main"
|
|
51
|
+
case .topRight: return "Top Right"
|
|
52
|
+
case .bottomLeft: return "Bottom Left"
|
|
53
|
+
case .bottomRight: return "Bottom Right"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
var shortLabel: String {
|
|
58
|
+
switch self {
|
|
59
|
+
case .overview: return "all"
|
|
60
|
+
case .main: return "main"
|
|
61
|
+
case .topRight: return "tr"
|
|
62
|
+
case .bottomLeft: return "bl"
|
|
63
|
+
case .bottomRight: return "br"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var keyHint: String {
|
|
68
|
+
switch self {
|
|
69
|
+
case .overview: return "0"
|
|
70
|
+
case .main: return "1"
|
|
71
|
+
case .topRight: return "2"
|
|
72
|
+
case .bottomLeft: return "3"
|
|
73
|
+
case .bottomRight: return "4"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
struct ScreenMapWindowSet: Identifiable, Equatable {
|
|
79
|
+
let id: UUID
|
|
80
|
+
var name: String
|
|
81
|
+
var windowIds: Set<UInt32>
|
|
82
|
+
|
|
83
|
+
init(id: UUID = UUID(), name: String, windowIds: Set<UInt32>) {
|
|
84
|
+
self.id = id
|
|
85
|
+
self.name = name
|
|
86
|
+
self.windowIds = windowIds
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var count: Int { windowIds.count }
|
|
90
|
+
}
|
|
91
|
+
|
|
13
92
|
// MARK: - Screen Map Window Entry
|
|
14
93
|
|
|
15
94
|
struct ScreenMapWindowEntry: Identifiable {
|
|
@@ -19,6 +98,7 @@ struct ScreenMapWindowEntry: Identifiable {
|
|
|
19
98
|
let title: String
|
|
20
99
|
var originalFrame: CGRect // frozen at snapshot time
|
|
21
100
|
var editedFrame: CGRect // mutated during drag
|
|
101
|
+
var virtualFrame: CGRect // persistent canvas/world position
|
|
22
102
|
let zIndex: Int // 0 = frontmost
|
|
23
103
|
var layer: Int // assigned by iterative peeling (per-display)
|
|
24
104
|
let displayIndex: Int // which monitor this window belongs to
|
|
@@ -72,6 +152,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
72
152
|
@Published var zoomLevel: CGFloat = 1.0 // 1.0 = fit-all
|
|
73
153
|
@Published var panOffset: CGPoint = .zero // canvas-local pixels
|
|
74
154
|
@Published var focusedDisplayIndex: Int? = nil // nil = all-displays view
|
|
155
|
+
@Published var activeViewportPreset: ScreenMapViewportPreset? = .main
|
|
75
156
|
@Published var windowSearchQuery: String = ""
|
|
76
157
|
@Published var isTilingMode: Bool = false
|
|
77
158
|
var isSearching: Bool { !windowSearchQuery.isEmpty }
|
|
@@ -179,6 +260,16 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
179
260
|
|
|
180
261
|
let actionLog = ScreenMapActionLog()
|
|
181
262
|
|
|
263
|
+
func syncLayoutFrame(at index: Int, to frame: CGRect) {
|
|
264
|
+
windows[index].virtualFrame = frame
|
|
265
|
+
windows[index].editedFrame = frame
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func resetLayoutFrameToOriginal(at index: Int) {
|
|
269
|
+
windows[index].virtualFrame = windows[index].originalFrame
|
|
270
|
+
windows[index].editedFrame = windows[index].originalFrame
|
|
271
|
+
}
|
|
272
|
+
|
|
182
273
|
/// Backward-compat: single active layer when exactly one is selected
|
|
183
274
|
var activeLayer: Int? {
|
|
184
275
|
selectedLayers.count == 1 ? selectedLayers.first : nil
|
|
@@ -197,6 +288,9 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
197
288
|
var mapOrigin: CGPoint = .zero
|
|
198
289
|
var screenSize: CGSize = .zero
|
|
199
290
|
var bboxOrigin: CGPoint = .zero // top-left of the bounding box in CG coords
|
|
291
|
+
var viewportSize: CGSize = .zero
|
|
292
|
+
var pendingCanvasNavigation: ScreenMapCanvasNavigationTarget?
|
|
293
|
+
var canvasNavigationRevision: Int = 0
|
|
200
294
|
|
|
201
295
|
let displays: [DisplayGeometry]
|
|
202
296
|
|
|
@@ -263,6 +357,169 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
263
357
|
return windows.filter { selectedLayers.contains($0.layer) }
|
|
264
358
|
}
|
|
265
359
|
|
|
360
|
+
private var worldScopedDisplays: [DisplayGeometry] {
|
|
361
|
+
guard let focusedDisplayIndex else { return displays }
|
|
362
|
+
return displays.filter { $0.index == focusedDisplayIndex }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private var worldScopedWindows: [ScreenMapWindowEntry] {
|
|
366
|
+
guard let focusedDisplayIndex else { return windows }
|
|
367
|
+
return windows.filter { $0.displayIndex == focusedDisplayIndex }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
var canvasWorldBounds: CGRect {
|
|
371
|
+
var rects = worldScopedDisplays.map(\.cgRect)
|
|
372
|
+
rects.append(contentsOf: worldScopedWindows.map(\.virtualFrame))
|
|
373
|
+
|
|
374
|
+
if rects.isEmpty {
|
|
375
|
+
return CGRect(origin: bboxOrigin, size: screenSize)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
var union = rects[0]
|
|
379
|
+
for rect in rects.dropFirst() {
|
|
380
|
+
union = union.union(rect)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let pad: CGFloat = focusedDisplayIndex == nil ? 120 : 80
|
|
384
|
+
return union.insetBy(dx: -pad, dy: -pad)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
var viewportWorldRect: CGRect {
|
|
388
|
+
guard scale > 0, viewportSize.width > 0, viewportSize.height > 0 else {
|
|
389
|
+
return canvasWorldBounds
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let raw = CGRect(
|
|
393
|
+
x: bboxOrigin.x - (mapOrigin.x + panOffset.x) / scale,
|
|
394
|
+
y: bboxOrigin.y - (mapOrigin.y + panOffset.y) / scale,
|
|
395
|
+
width: viewportSize.width / scale,
|
|
396
|
+
height: viewportSize.height / scale
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
let world = canvasWorldBounds
|
|
400
|
+
let clipped = raw.intersection(world)
|
|
401
|
+
return clipped.isNull ? raw : clipped
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
func viewportRect(for preset: ScreenMapViewportPreset) -> CGRect {
|
|
405
|
+
let world = canvasWorldBounds
|
|
406
|
+
guard preset != .overview else { return world }
|
|
407
|
+
|
|
408
|
+
let halfWidth = max(world.width / 2, 1)
|
|
409
|
+
let halfHeight = max(world.height / 2, 1)
|
|
410
|
+
|
|
411
|
+
switch preset {
|
|
412
|
+
case .overview:
|
|
413
|
+
return world
|
|
414
|
+
case .main:
|
|
415
|
+
return CGRect(x: world.minX, y: world.minY, width: halfWidth, height: halfHeight)
|
|
416
|
+
case .topRight:
|
|
417
|
+
return CGRect(x: world.midX, y: world.minY, width: halfWidth, height: halfHeight)
|
|
418
|
+
case .bottomLeft:
|
|
419
|
+
return CGRect(x: world.minX, y: world.midY, width: halfWidth, height: halfHeight)
|
|
420
|
+
case .bottomRight:
|
|
421
|
+
return CGRect(x: world.midX, y: world.midY, width: halfWidth, height: halfHeight)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
var viewportPresetSummary: String {
|
|
426
|
+
activeViewportPreset?.title ?? "Custom"
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
func windows(matching ids: Set<UInt32>) -> [ScreenMapWindowEntry] {
|
|
430
|
+
windows.filter { ids.contains($0.id) }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
func rectForWindowIDs(_ ids: Set<UInt32>) -> CGRect? {
|
|
434
|
+
let matched = windows(matching: ids)
|
|
435
|
+
guard let first = matched.first else { return nil }
|
|
436
|
+
return matched.dropFirst().reduce(first.virtualFrame) { $0.union($1.virtualFrame) }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
var canvasExplorerRegions: [ScreenMapCanvasRegion] {
|
|
440
|
+
let world = canvasWorldBounds
|
|
441
|
+
var regions: [ScreenMapCanvasRegion] = [
|
|
442
|
+
ScreenMapCanvasRegion(
|
|
443
|
+
id: "overview",
|
|
444
|
+
kind: .overview,
|
|
445
|
+
title: focusedDisplayIndex == nil ? "All Displays" : "Display Canvas",
|
|
446
|
+
subtitle: "\(worldScopedWindows.count) windows",
|
|
447
|
+
rect: world,
|
|
448
|
+
count: worldScopedWindows.count,
|
|
449
|
+
displayIndex: focusedDisplayIndex,
|
|
450
|
+
layer: nil
|
|
451
|
+
)
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
if focusedDisplayIndex == nil {
|
|
455
|
+
for display in spatialDisplayOrder {
|
|
456
|
+
let displayWindows = windows.filter { $0.displayIndex == display.index }
|
|
457
|
+
let rect = regionRect(
|
|
458
|
+
for: displayWindows,
|
|
459
|
+
fallback: display.cgRect,
|
|
460
|
+
padding: 60
|
|
461
|
+
)
|
|
462
|
+
regions.append(
|
|
463
|
+
ScreenMapCanvasRegion(
|
|
464
|
+
id: "display-\(display.index)",
|
|
465
|
+
kind: .display,
|
|
466
|
+
title: display.label,
|
|
467
|
+
subtitle: "\(displayWindows.count) windows",
|
|
468
|
+
rect: rect,
|
|
469
|
+
count: displayWindows.count,
|
|
470
|
+
displayIndex: display.index,
|
|
471
|
+
layer: nil
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let layerScope = effectiveLayers.compactMap { layer -> ScreenMapCanvasRegion? in
|
|
478
|
+
let layerWindows = worldScopedWindows.filter { $0.layer == layer }
|
|
479
|
+
guard !layerWindows.isEmpty else { return nil }
|
|
480
|
+
|
|
481
|
+
let displayLabel: String = {
|
|
482
|
+
guard focusedDisplayIndex == nil,
|
|
483
|
+
let displayIndex = layerWindows.first?.displayIndex,
|
|
484
|
+
let display = displays.first(where: { $0.index == displayIndex }) else {
|
|
485
|
+
return ""
|
|
486
|
+
}
|
|
487
|
+
return display.label
|
|
488
|
+
}()
|
|
489
|
+
|
|
490
|
+
let subtitleBase = "\(layerWindows.count) window\(layerWindows.count == 1 ? "" : "s")"
|
|
491
|
+
let subtitle = displayLabel.isEmpty ? subtitleBase : "\(subtitleBase) · \(displayLabel)"
|
|
492
|
+
|
|
493
|
+
return ScreenMapCanvasRegion(
|
|
494
|
+
id: "layer-\(layer)-\(focusedDisplayIndex.map(String.init) ?? "all")",
|
|
495
|
+
kind: .layer,
|
|
496
|
+
title: layerNames[layer] ?? "Layer \(layer)",
|
|
497
|
+
subtitle: subtitle,
|
|
498
|
+
rect: regionRect(for: layerWindows, fallback: layerWindows[0].virtualFrame, padding: 48),
|
|
499
|
+
count: layerWindows.count,
|
|
500
|
+
displayIndex: layerWindows.first?.displayIndex,
|
|
501
|
+
layer: layer
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
.sorted { lhs, rhs in
|
|
505
|
+
if lhs.count != rhs.count { return lhs.count > rhs.count }
|
|
506
|
+
return lhs.title < rhs.title
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
regions.append(contentsOf: layerScope.prefix(6))
|
|
510
|
+
return regions
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private func regionRect(for windows: [ScreenMapWindowEntry], fallback: CGRect, padding: CGFloat) -> CGRect {
|
|
514
|
+
guard !windows.isEmpty else { return fallback.insetBy(dx: -padding, dy: -padding) }
|
|
515
|
+
|
|
516
|
+
var union = windows[0].virtualFrame
|
|
517
|
+
for win in windows.dropFirst() {
|
|
518
|
+
union = union.union(win.virtualFrame)
|
|
519
|
+
}
|
|
520
|
+
return union.insetBy(dx: -padding, dy: -padding)
|
|
521
|
+
}
|
|
522
|
+
|
|
266
523
|
/// The focused display geometry (nil when showing all)
|
|
267
524
|
var focusedDisplay: DisplayGeometry? {
|
|
268
525
|
guard let idx = focusedDisplayIndex else { return nil }
|
|
@@ -300,6 +557,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
300
557
|
focusedDisplayIndex = index
|
|
301
558
|
selectedLayers = [] // reset to "All" for the new display scope
|
|
302
559
|
resetZoomPan()
|
|
560
|
+
DiagnosticLog.shared.info("[Canvas] scope → \(canvasScopeSummary)")
|
|
303
561
|
}
|
|
304
562
|
|
|
305
563
|
/// Cycle to the next display in spatial (left-to-right) order
|
|
@@ -310,6 +568,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
310
568
|
focusedDisplayIndex = order.first!.index
|
|
311
569
|
selectedLayers = []
|
|
312
570
|
resetZoomPan()
|
|
571
|
+
DiagnosticLog.shared.info("[Canvas] scope → \(canvasScopeSummary)")
|
|
313
572
|
return
|
|
314
573
|
}
|
|
315
574
|
if let pos = order.firstIndex(where: { $0.index == current }) {
|
|
@@ -324,6 +583,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
324
583
|
}
|
|
325
584
|
selectedLayers = []
|
|
326
585
|
resetZoomPan()
|
|
586
|
+
DiagnosticLog.shared.info("[Canvas] scope → \(canvasScopeSummary)")
|
|
327
587
|
}
|
|
328
588
|
|
|
329
589
|
/// Cycle to the previous display in spatial (right-to-left) order
|
|
@@ -334,6 +594,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
334
594
|
focusedDisplayIndex = order.last!.index
|
|
335
595
|
selectedLayers = []
|
|
336
596
|
resetZoomPan()
|
|
597
|
+
DiagnosticLog.shared.info("[Canvas] scope → \(canvasScopeSummary)")
|
|
337
598
|
return
|
|
338
599
|
}
|
|
339
600
|
if let pos = order.firstIndex(where: { $0.index == current }) {
|
|
@@ -347,6 +608,15 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
347
608
|
}
|
|
348
609
|
selectedLayers = []
|
|
349
610
|
resetZoomPan()
|
|
611
|
+
DiagnosticLog.shared.info("[Canvas] scope → \(canvasScopeSummary)")
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
var canvasScopeSummary: String {
|
|
615
|
+
guard let focusedDisplayIndex,
|
|
616
|
+
let display = displays.first(where: { $0.index == focusedDisplayIndex }) else {
|
|
617
|
+
return "all displays"
|
|
618
|
+
}
|
|
619
|
+
return "display \(spatialNumber(for: focusedDisplayIndex)) · \(display.label)"
|
|
350
620
|
}
|
|
351
621
|
|
|
352
622
|
/// Number of windows with pending edits (position or size)
|
|
@@ -429,12 +699,12 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
429
699
|
/// Move a window to a different layer
|
|
430
700
|
func reassignLayer(windowId: UInt32, toLayer: Int, fitToAvailable: Bool) {
|
|
431
701
|
guard let idx = windows.firstIndex(where: { $0.id == windowId }) else { return }
|
|
432
|
-
let oldFrame = windows[idx].
|
|
702
|
+
let oldFrame = windows[idx].virtualFrame
|
|
433
703
|
windows[idx].layer = toLayer
|
|
434
704
|
if fitToAvailable {
|
|
435
705
|
fitWindowIntoLayer(at: idx)
|
|
436
706
|
}
|
|
437
|
-
let newFrame = windows[idx].
|
|
707
|
+
let newFrame = windows[idx].virtualFrame
|
|
438
708
|
if oldFrame != newFrame {
|
|
439
709
|
DiagnosticLog.shared.info("[ScreenMap] reassign wid=\(windowId): fitted \(Int(oldFrame.origin.x)),\(Int(oldFrame.origin.y)) → \(Int(newFrame.origin.x)),\(Int(newFrame.origin.y))")
|
|
440
710
|
}
|
|
@@ -444,10 +714,10 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
444
714
|
func fitWindowIntoLayer(at idx: Int) {
|
|
445
715
|
let win = windows[idx]
|
|
446
716
|
let siblings = windows.enumerated().filter { $0.offset != idx && $0.element.layer == win.layer }
|
|
447
|
-
let siblingFrames = siblings.map(\.element.
|
|
717
|
+
let siblingFrames = siblings.map(\.element.virtualFrame)
|
|
448
718
|
let screenRect = CGRect(origin: .zero, size: screenSize)
|
|
449
|
-
if let fitted = fitRect(win.
|
|
450
|
-
|
|
719
|
+
if let fitted = fitRect(win.virtualFrame, avoiding: siblingFrames, within: screenRect) {
|
|
720
|
+
syncLayoutFrame(at: idx, to: fitted)
|
|
451
721
|
}
|
|
452
722
|
}
|
|
453
723
|
|
|
@@ -508,7 +778,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
508
778
|
|
|
509
779
|
if indices.count == 1 {
|
|
510
780
|
let frame = CGRect(x: visible.origin.x, y: axTop, width: visible.width, height: visible.height)
|
|
511
|
-
|
|
781
|
+
syncLayoutFrame(at: indices[0], to: frame)
|
|
512
782
|
totalTiled += 1
|
|
513
783
|
continue
|
|
514
784
|
}
|
|
@@ -529,7 +799,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
529
799
|
let x0 = baseX + (col * totalW) / cols
|
|
530
800
|
let x1 = baseX + ((col + 1) * totalW) / cols
|
|
531
801
|
let frame = CGRect(x: CGFloat(x0), y: CGFloat(y0), width: CGFloat(x1 - x0), height: CGFloat(y1 - y0))
|
|
532
|
-
|
|
802
|
+
syncLayoutFrame(at: indices[slotIdx], to: frame)
|
|
533
803
|
slotIdx += 1
|
|
534
804
|
}
|
|
535
805
|
}
|
|
@@ -568,7 +838,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
568
838
|
for col in 0..<cols {
|
|
569
839
|
guard slotIdx < indices.count else { break }
|
|
570
840
|
let idx = indices[slotIdx]
|
|
571
|
-
let orig = windows[idx].
|
|
841
|
+
let orig = windows[idx].virtualFrame
|
|
572
842
|
|
|
573
843
|
let cellX = visible.origin.x + CGFloat(col) * colW + padding
|
|
574
844
|
let cellY = axY + padding
|
|
@@ -586,7 +856,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
586
856
|
let x = cellX + (cellW - fitW) / 2
|
|
587
857
|
let y = cellY + (cellH - fitH) / 2
|
|
588
858
|
|
|
589
|
-
|
|
859
|
+
syncLayoutFrame(at: idx, to: CGRect(x: x, y: y, width: fitW, height: fitH))
|
|
590
860
|
slotIdx += 1
|
|
591
861
|
}
|
|
592
862
|
}
|
|
@@ -621,8 +891,8 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
621
891
|
for j in (i + 1)..<indices.count {
|
|
622
892
|
let idxA = indices[i]
|
|
623
893
|
let idxB = indices[j]
|
|
624
|
-
let a = windows[idxA].
|
|
625
|
-
let b = windows[idxB].
|
|
894
|
+
let a = windows[idxA].virtualFrame
|
|
895
|
+
let b = windows[idxB].virtualFrame
|
|
626
896
|
guard a.intersects(b) else { continue }
|
|
627
897
|
hadOverlap = true
|
|
628
898
|
|
|
@@ -631,22 +901,30 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
631
901
|
|
|
632
902
|
if overlapW < overlapH {
|
|
633
903
|
let push = (overlapW / 2).rounded(.up) + 1
|
|
904
|
+
var newA = a
|
|
905
|
+
var newB = b
|
|
634
906
|
if a.midX <= b.midX {
|
|
635
|
-
|
|
636
|
-
|
|
907
|
+
newA.origin.x -= push
|
|
908
|
+
newB.origin.x += push
|
|
637
909
|
} else {
|
|
638
|
-
|
|
639
|
-
|
|
910
|
+
newA.origin.x += push
|
|
911
|
+
newB.origin.x -= push
|
|
640
912
|
}
|
|
913
|
+
syncLayoutFrame(at: idxA, to: newA)
|
|
914
|
+
syncLayoutFrame(at: idxB, to: newB)
|
|
641
915
|
} else {
|
|
642
916
|
let push = (overlapH / 2).rounded(.up) + 1
|
|
917
|
+
var newA = a
|
|
918
|
+
var newB = b
|
|
643
919
|
if a.midY <= b.midY {
|
|
644
|
-
|
|
645
|
-
|
|
920
|
+
newA.origin.y -= push
|
|
921
|
+
newB.origin.y += push
|
|
646
922
|
} else {
|
|
647
|
-
|
|
648
|
-
|
|
923
|
+
newA.origin.y += push
|
|
924
|
+
newB.origin.y -= push
|
|
649
925
|
}
|
|
926
|
+
syncLayoutFrame(at: idxA, to: newA)
|
|
927
|
+
syncLayoutFrame(at: idxB, to: newB)
|
|
650
928
|
}
|
|
651
929
|
affected.insert(idxA)
|
|
652
930
|
affected.insert(idxB)
|
|
@@ -661,12 +939,12 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
661
939
|
}
|
|
662
940
|
|
|
663
941
|
private func clampToScreen(at idx: Int, bounds: CGRect) {
|
|
664
|
-
var f = windows[idx].
|
|
942
|
+
var f = windows[idx].virtualFrame
|
|
665
943
|
if f.minX < bounds.minX { f.origin.x = bounds.minX }
|
|
666
944
|
if f.minY < bounds.minY { f.origin.y = bounds.minY }
|
|
667
945
|
if f.maxX > bounds.maxX { f.origin.x = bounds.maxX - f.width }
|
|
668
946
|
if f.maxY > bounds.maxY { f.origin.y = bounds.maxY - f.height }
|
|
669
|
-
|
|
947
|
+
syncLayoutFrame(at: idx, to: f)
|
|
670
948
|
}
|
|
671
949
|
|
|
672
950
|
/// Grow each window outward until it hits a neighbor or screen edge
|
|
@@ -691,7 +969,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
691
969
|
width: screen.frame.width, height: screen.frame.height)
|
|
692
970
|
|
|
693
971
|
// Snapshot original positions for neighbor detection
|
|
694
|
-
let origFrames = indices.map { windows[$0].
|
|
972
|
+
let origFrames = indices.map { windows[$0].virtualFrame }
|
|
695
973
|
|
|
696
974
|
for (i, idx) in indices.enumerated() {
|
|
697
975
|
let me = origFrames[i]
|
|
@@ -726,8 +1004,8 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
726
1004
|
}
|
|
727
1005
|
|
|
728
1006
|
let newFrame = CGRect(x: left, y: top, width: right - left, height: bottom - top)
|
|
729
|
-
if newFrame != windows[idx].
|
|
730
|
-
|
|
1007
|
+
if newFrame != windows[idx].virtualFrame {
|
|
1008
|
+
syncLayoutFrame(at: idx, to: newFrame)
|
|
731
1009
|
totalAffected += 1
|
|
732
1010
|
}
|
|
733
1011
|
}
|
|
@@ -767,7 +1045,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
767
1045
|
guard slots.count == indices.count else { continue }
|
|
768
1046
|
|
|
769
1047
|
for (i, idx) in indices.enumerated() {
|
|
770
|
-
|
|
1048
|
+
syncLayoutFrame(at: idx, to: slots[i])
|
|
771
1049
|
}
|
|
772
1050
|
totalDistributed += indices.count
|
|
773
1051
|
}
|
|
@@ -777,7 +1055,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
777
1055
|
/// Reset all edited frames back to original
|
|
778
1056
|
func discardEdits() {
|
|
779
1057
|
for i in windows.indices {
|
|
780
|
-
|
|
1058
|
+
resetLayoutFrameToOriginal(at: i)
|
|
781
1059
|
}
|
|
782
1060
|
}
|
|
783
1061
|
|
|
@@ -813,15 +1091,15 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
813
1091
|
let siblings = windows.enumerated().filter {
|
|
814
1092
|
$0.offset != idx && $0.element.layer == targetLayer &&
|
|
815
1093
|
(dIdx == nil || $0.element.displayIndex == dIdx!)
|
|
816
|
-
}.map(\.element.
|
|
1094
|
+
}.map(\.element.virtualFrame)
|
|
817
1095
|
|
|
818
|
-
let collisions = siblings.filter { $0.intersects(win.
|
|
1096
|
+
let collisions = siblings.filter { $0.intersects(win.virtualFrame) }
|
|
819
1097
|
if collisions.isEmpty {
|
|
820
1098
|
windows[idx].layer = targetLayer
|
|
821
1099
|
break
|
|
822
1100
|
}
|
|
823
|
-
if let fitted = fitRect(win.
|
|
824
|
-
|
|
1101
|
+
if let fitted = fitRect(win.virtualFrame, avoiding: siblings, within: screenRect) {
|
|
1102
|
+
syncLayoutFrame(at: idx, to: fitted)
|
|
825
1103
|
windows[idx].layer = targetLayer
|
|
826
1104
|
break
|
|
827
1105
|
}
|
|
@@ -921,8 +1199,8 @@ final class ScreenMapActionLog {
|
|
|
921
1199
|
WindowSnapshot(
|
|
922
1200
|
wid: win.id, app: win.app, title: win.title,
|
|
923
1201
|
frame: .init(
|
|
924
|
-
x: Int(win.
|
|
925
|
-
w: Int(win.
|
|
1202
|
+
x: Int(win.virtualFrame.origin.x), y: Int(win.virtualFrame.origin.y),
|
|
1203
|
+
w: Int(win.virtualFrame.width), h: Int(win.virtualFrame.height)
|
|
926
1204
|
),
|
|
927
1205
|
layer: win.layer
|
|
928
1206
|
)
|
|
@@ -1074,6 +1352,8 @@ final class ScreenMapActionLog {
|
|
|
1074
1352
|
final class ScreenMapController: ObservableObject {
|
|
1075
1353
|
@Published var editor: ScreenMapEditorState?
|
|
1076
1354
|
@Published var selectedWindowIds: Set<UInt32> = []
|
|
1355
|
+
@Published var windowSets: [ScreenMapWindowSet] = []
|
|
1356
|
+
@Published var activeWindowSetID: UUID? = nil
|
|
1077
1357
|
@Published var flashMessage: String? = nil
|
|
1078
1358
|
@Published var previewCaptures: [UInt32: NSImage] = [:]
|
|
1079
1359
|
@Published var savedPositions: [UInt32: (pid: Int32, frame: WindowFrame)]? = nil
|
|
@@ -1095,9 +1375,15 @@ final class ScreenMapController: ObservableObject {
|
|
|
1095
1375
|
|
|
1096
1376
|
func isSelected(_ id: UInt32) -> Bool { selectedWindowIds.contains(id) }
|
|
1097
1377
|
|
|
1378
|
+
var activeWindowSet: ScreenMapWindowSet? {
|
|
1379
|
+
guard let activeWindowSetID else { return nil }
|
|
1380
|
+
return windowSets.first(where: { $0.id == activeWindowSetID })
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1098
1383
|
func selectSingle(_ id: UInt32) {
|
|
1099
1384
|
navigateToWindowDisplay(id)
|
|
1100
1385
|
selectedWindowIds = [id]
|
|
1386
|
+
activeWindowSetID = nil
|
|
1101
1387
|
}
|
|
1102
1388
|
|
|
1103
1389
|
func toggleSelection(_ id: UInt32) {
|
|
@@ -1106,10 +1392,12 @@ final class ScreenMapController: ObservableObject {
|
|
|
1106
1392
|
} else {
|
|
1107
1393
|
selectedWindowIds.insert(id)
|
|
1108
1394
|
}
|
|
1395
|
+
activeWindowSetID = nil
|
|
1109
1396
|
}
|
|
1110
1397
|
|
|
1111
1398
|
func clearSelection() {
|
|
1112
1399
|
selectedWindowIds = []
|
|
1400
|
+
activeWindowSetID = nil
|
|
1113
1401
|
}
|
|
1114
1402
|
|
|
1115
1403
|
func selectNextWindow() {
|
|
@@ -1123,6 +1411,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1123
1411
|
} else {
|
|
1124
1412
|
selectedWindowIds = [wins[0].id]
|
|
1125
1413
|
}
|
|
1414
|
+
activeWindowSetID = nil
|
|
1126
1415
|
objectWillChange.send()
|
|
1127
1416
|
}
|
|
1128
1417
|
|
|
@@ -1137,6 +1426,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1137
1426
|
} else {
|
|
1138
1427
|
selectedWindowIds = [wins[wins.count - 1].id]
|
|
1139
1428
|
}
|
|
1429
|
+
activeWindowSetID = nil
|
|
1140
1430
|
objectWillChange.send()
|
|
1141
1431
|
}
|
|
1142
1432
|
|
|
@@ -1144,10 +1434,58 @@ final class ScreenMapController: ObservableObject {
|
|
|
1144
1434
|
guard let ed = editor else { return }
|
|
1145
1435
|
let allIds = Set(ed.focusedVisibleWindows.map(\.id))
|
|
1146
1436
|
selectedWindowIds = allIds
|
|
1437
|
+
activeWindowSetID = nil
|
|
1147
1438
|
flash("Selected \(allIds.count) windows")
|
|
1148
1439
|
objectWillChange.send()
|
|
1149
1440
|
}
|
|
1150
1441
|
|
|
1442
|
+
// MARK: - Window Sets
|
|
1443
|
+
|
|
1444
|
+
func createWindowSetFromSelection() {
|
|
1445
|
+
let ids = selectedWindowIds
|
|
1446
|
+
guard !ids.isEmpty else {
|
|
1447
|
+
flash("Select windows first")
|
|
1448
|
+
return
|
|
1449
|
+
}
|
|
1450
|
+
if let existing = windowSets.first(where: { $0.windowIds == ids }) {
|
|
1451
|
+
activeWindowSetID = existing.id
|
|
1452
|
+
flash("\(existing.name) already saved")
|
|
1453
|
+
return
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
let set = ScreenMapWindowSet(name: "Set \(windowSets.count + 1)", windowIds: ids)
|
|
1457
|
+
windowSets.append(set)
|
|
1458
|
+
activeWindowSetID = set.id
|
|
1459
|
+
flash("Saved \(set.name)")
|
|
1460
|
+
objectWillChange.send()
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
func focusWindowSet(_ set: ScreenMapWindowSet) {
|
|
1464
|
+
guard let ed = editor else { return }
|
|
1465
|
+
let liveIds = Set(ed.windows(matching: set.windowIds).map(\.id))
|
|
1466
|
+
guard !liveIds.isEmpty else {
|
|
1467
|
+
flash("\(set.name) is empty")
|
|
1468
|
+
return
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
selectedWindowIds = liveIds
|
|
1472
|
+
activeWindowSetID = set.id
|
|
1473
|
+
if let rect = ed.rectForWindowIDs(liveIds) {
|
|
1474
|
+
focusCanvas(on: rect.insetBy(dx: -80, dy: -80), zoomToFit: true)
|
|
1475
|
+
}
|
|
1476
|
+
flash(set.name)
|
|
1477
|
+
objectWillChange.send()
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
func deleteWindowSet(_ set: ScreenMapWindowSet) {
|
|
1481
|
+
windowSets.removeAll { $0.id == set.id }
|
|
1482
|
+
if activeWindowSetID == set.id {
|
|
1483
|
+
activeWindowSetID = nil
|
|
1484
|
+
}
|
|
1485
|
+
flash("Removed \(set.name)")
|
|
1486
|
+
objectWillChange.send()
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1151
1489
|
// MARK: - Search
|
|
1152
1490
|
|
|
1153
1491
|
var searchHighlightedWindowId: UInt32? {
|
|
@@ -1205,14 +1543,9 @@ final class ScreenMapController: ObservableObject {
|
|
|
1205
1543
|
let win = ed.windows.first(where: { $0.id == windowId }) else { return }
|
|
1206
1544
|
if isSearchActive { closeSearch() }
|
|
1207
1545
|
selectSingle(windowId)
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
let winCopy = win
|
|
1212
|
-
let edCopy = ed
|
|
1213
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
1214
|
-
WindowBezel.shared.show(for: winCopy, editor: edCopy)
|
|
1215
|
-
}
|
|
1546
|
+
// Raise the target window and let it stay on top (don't re-activate Lattices)
|
|
1547
|
+
WindowTiler.focusWindow(wid: win.id, pid: win.pid)
|
|
1548
|
+
WindowTiler.highlightWindowById(wid: win.id)
|
|
1216
1549
|
}
|
|
1217
1550
|
|
|
1218
1551
|
func focusSelectedWindowOnScreen() {
|
|
@@ -1234,6 +1567,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1234
1567
|
// MARK: - Enter
|
|
1235
1568
|
|
|
1236
1569
|
func enter() {
|
|
1570
|
+
let existingSets = windowSets
|
|
1237
1571
|
guard let windowList = CGWindowListCopyWindowInfo(
|
|
1238
1572
|
[.optionAll, .excludeDesktopElements], kCGNullWindowID
|
|
1239
1573
|
) as? [[String: Any]] else { return }
|
|
@@ -1372,7 +1706,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1372
1706
|
|
|
1373
1707
|
mapWindows.append(ScreenMapWindowEntry(
|
|
1374
1708
|
id: win.wid, pid: win.pid, app: win.app, title: win.title,
|
|
1375
|
-
originalFrame: win.frame, editedFrame: win.frame,
|
|
1709
|
+
originalFrame: win.frame, editedFrame: win.frame, virtualFrame: win.frame,
|
|
1376
1710
|
zIndex: i, layer: assignedLayer, displayIndex: win.displayIndex,
|
|
1377
1711
|
isOnScreen: win.isOnScreen,
|
|
1378
1712
|
latticesSession: latticesSession ?? ctx?.session,
|
|
@@ -1415,20 +1749,180 @@ final class ScreenMapController: ObservableObject {
|
|
|
1415
1749
|
}
|
|
1416
1750
|
|
|
1417
1751
|
editor = newEditor
|
|
1752
|
+
let liveIds = Set(mapWindows.map(\.id))
|
|
1753
|
+
windowSets = existingSets.compactMap { set in
|
|
1754
|
+
let filtered = set.windowIds.intersection(liveIds)
|
|
1755
|
+
guard !filtered.isEmpty else { return nil }
|
|
1756
|
+
return ScreenMapWindowSet(id: set.id, name: set.name, windowIds: filtered)
|
|
1757
|
+
}
|
|
1758
|
+
if let activeWindowSetID, !windowSets.contains(where: { $0.id == activeWindowSetID }) {
|
|
1759
|
+
self.activeWindowSetID = nil
|
|
1760
|
+
}
|
|
1418
1761
|
selectedWindowIds = []
|
|
1762
|
+
focusViewportPreset(.main, flashView: false)
|
|
1419
1763
|
}
|
|
1420
1764
|
|
|
1421
1765
|
/// Re-snapshot, preserving display/layer context
|
|
1422
1766
|
func refresh() {
|
|
1423
1767
|
let savedDisplay = editor?.focusedDisplayIndex
|
|
1424
1768
|
let savedLayers = editor?.selectedLayers ?? []
|
|
1769
|
+
let savedViewportPreset = editor?.activeViewportPreset
|
|
1425
1770
|
enter()
|
|
1426
1771
|
if let ed = editor {
|
|
1427
1772
|
ed.focusedDisplayIndex = savedDisplay
|
|
1428
1773
|
ed.selectedLayers = savedLayers
|
|
1774
|
+
if let savedViewportPreset {
|
|
1775
|
+
focusViewportPreset(savedViewportPreset, flashView: false)
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// MARK: - Canvas Navigation
|
|
1781
|
+
|
|
1782
|
+
func recenterViewport(at worldPoint: CGPoint) {
|
|
1783
|
+
editor?.activeViewportPreset = nil
|
|
1784
|
+
queueCanvasNavigation(centeredOn: worldPoint, rect: nil, zoomToFit: false)
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
func focusCanvas(on rect: CGRect, focusDisplay displayIndex: Int? = nil, resetDisplayFocus: Bool = false, zoomToFit: Bool = true) {
|
|
1788
|
+
guard let ed = editor else { return }
|
|
1789
|
+
|
|
1790
|
+
if let displayIndex {
|
|
1791
|
+
ed.focusDisplay(displayIndex)
|
|
1792
|
+
} else if resetDisplayFocus {
|
|
1793
|
+
ed.focusDisplay(nil)
|
|
1794
|
+
}
|
|
1795
|
+
ed.activeViewportPreset = nil
|
|
1796
|
+
|
|
1797
|
+
queueCanvasNavigation(
|
|
1798
|
+
centeredOn: CGPoint(x: rect.midX, y: rect.midY),
|
|
1799
|
+
rect: rect,
|
|
1800
|
+
zoomToFit: zoomToFit
|
|
1801
|
+
)
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
func focusViewportPreset(_ preset: ScreenMapViewportPreset, flashView: Bool = true) {
|
|
1805
|
+
guard let ed = editor else { return }
|
|
1806
|
+
ed.activeViewportPreset = preset
|
|
1807
|
+
let rect = ed.viewportRect(for: preset)
|
|
1808
|
+
DiagnosticLog.shared.info("[Canvas] preset → \(preset.title)")
|
|
1809
|
+
queueCanvasNavigation(
|
|
1810
|
+
centeredOn: CGPoint(x: rect.midX, y: rect.midY),
|
|
1811
|
+
rect: rect,
|
|
1812
|
+
zoomToFit: true
|
|
1813
|
+
)
|
|
1814
|
+
if flashView {
|
|
1815
|
+
flash(preset.title)
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
func jumpToCanvasRegion(_ region: ScreenMapCanvasRegion) {
|
|
1820
|
+
DiagnosticLog.shared.info("[Canvas] jump → \(region.title) · \(region.subtitle)")
|
|
1821
|
+
switch region.kind {
|
|
1822
|
+
case .overview:
|
|
1823
|
+
focusViewportPreset(.overview)
|
|
1824
|
+
case .display:
|
|
1825
|
+
focusCanvas(on: region.rect, focusDisplay: region.displayIndex, zoomToFit: true)
|
|
1826
|
+
case .layer:
|
|
1827
|
+
focusCanvas(on: region.rect, zoomToFit: true)
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
func applyPendingCanvasNavigationIfNeeded() {
|
|
1832
|
+
guard let ed = editor, let target = ed.pendingCanvasNavigation else { return }
|
|
1833
|
+
ed.pendingCanvasNavigation = nil
|
|
1834
|
+
let shouldLogView = target.rect != nil || target.zoomToFit
|
|
1835
|
+
|
|
1836
|
+
var targetZoom: CGFloat? = nil
|
|
1837
|
+
if target.zoomToFit,
|
|
1838
|
+
let rect = target.rect,
|
|
1839
|
+
ed.fitScale > 0,
|
|
1840
|
+
ed.viewportSize.width > 0,
|
|
1841
|
+
ed.viewportSize.height > 0 {
|
|
1842
|
+
let paddedW = max(rect.width, 120)
|
|
1843
|
+
let paddedH = max(rect.height, 80)
|
|
1844
|
+
let desiredScale = min(
|
|
1845
|
+
(ed.viewportSize.width * 0.84) / paddedW,
|
|
1846
|
+
(ed.viewportSize.height * 0.84) / paddedH
|
|
1847
|
+
)
|
|
1848
|
+
targetZoom = max(
|
|
1849
|
+
ScreenMapEditorState.minZoom,
|
|
1850
|
+
min(ScreenMapEditorState.maxZoom, desiredScale / ed.fitScale)
|
|
1851
|
+
)
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
setViewport(centeredOn: target.center, zoomLevel: targetZoom)
|
|
1855
|
+
if shouldLogView {
|
|
1856
|
+
let viewport = ed.viewportWorldRect
|
|
1857
|
+
DiagnosticLog.shared.info(
|
|
1858
|
+
"[Canvas] view → \(ed.canvasScopeSummary) center=(\(Int(viewport.midX)),\(Int(viewport.midY))) viewport=\(Int(viewport.width))×\(Int(viewport.height)) zoom=\(Int(ed.zoomLevel * 100))%"
|
|
1859
|
+
)
|
|
1429
1860
|
}
|
|
1430
1861
|
}
|
|
1431
1862
|
|
|
1863
|
+
private func queueCanvasNavigation(centeredOn targetCenter: CGPoint, rect: CGRect?, zoomToFit: Bool) {
|
|
1864
|
+
guard let ed = editor else { return }
|
|
1865
|
+
ed.pendingCanvasNavigation = ScreenMapCanvasNavigationTarget(
|
|
1866
|
+
center: targetCenter,
|
|
1867
|
+
rect: rect,
|
|
1868
|
+
zoomToFit: zoomToFit
|
|
1869
|
+
)
|
|
1870
|
+
ed.canvasNavigationRevision &+= 1
|
|
1871
|
+
ed.objectWillChange.send()
|
|
1872
|
+
objectWillChange.send()
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
private func setViewport(centeredOn targetCenter: CGPoint, zoomLevel targetZoom: CGFloat?) {
|
|
1876
|
+
guard let ed = editor else { return }
|
|
1877
|
+
|
|
1878
|
+
if let targetZoom {
|
|
1879
|
+
ed.zoomLevel = targetZoom
|
|
1880
|
+
ed.scale = ed.fitScale * targetZoom
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
let effectiveScale = max(ed.fitScale * ed.zoomLevel, 0.0001)
|
|
1884
|
+
let viewport = CGSize(
|
|
1885
|
+
width: max(ed.viewportSize.width, 1),
|
|
1886
|
+
height: max(ed.viewportSize.height, 1)
|
|
1887
|
+
)
|
|
1888
|
+
let viewportWorld = CGSize(
|
|
1889
|
+
width: viewport.width / effectiveScale,
|
|
1890
|
+
height: viewport.height / effectiveScale
|
|
1891
|
+
)
|
|
1892
|
+
let world = ed.canvasWorldBounds
|
|
1893
|
+
|
|
1894
|
+
let clampedCenter = CGPoint(
|
|
1895
|
+
x: clampedViewportCenter(
|
|
1896
|
+
target: targetCenter.x,
|
|
1897
|
+
minEdge: world.minX,
|
|
1898
|
+
maxEdge: world.maxX,
|
|
1899
|
+
viewportExtent: viewportWorld.width
|
|
1900
|
+
),
|
|
1901
|
+
y: clampedViewportCenter(
|
|
1902
|
+
target: targetCenter.y,
|
|
1903
|
+
minEdge: world.minY,
|
|
1904
|
+
maxEdge: world.maxY,
|
|
1905
|
+
viewportExtent: viewportWorld.height
|
|
1906
|
+
)
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1909
|
+
ed.panOffset = CGPoint(
|
|
1910
|
+
x: viewport.width / 2 - ed.mapOrigin.x - (clampedCenter.x - ed.bboxOrigin.x) * effectiveScale,
|
|
1911
|
+
y: viewport.height / 2 - ed.mapOrigin.y - (clampedCenter.y - ed.bboxOrigin.y) * effectiveScale
|
|
1912
|
+
)
|
|
1913
|
+
ed.objectWillChange.send()
|
|
1914
|
+
objectWillChange.send()
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
private func clampedViewportCenter(target: CGFloat, minEdge: CGFloat, maxEdge: CGFloat, viewportExtent: CGFloat) -> CGFloat {
|
|
1918
|
+
if maxEdge - minEdge <= viewportExtent {
|
|
1919
|
+
return (minEdge + maxEdge) / 2
|
|
1920
|
+
}
|
|
1921
|
+
let minCenter = minEdge + viewportExtent / 2
|
|
1922
|
+
let maxCenter = maxEdge - viewportExtent / 2
|
|
1923
|
+
return min(max(target, minCenter), maxCenter)
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1432
1926
|
// MARK: - Key Handler
|
|
1433
1927
|
|
|
1434
1928
|
func handleKey(_ keyCode: UInt16, modifiers: NSEvent.ModifierFlags = []) -> Bool {
|
|
@@ -1560,6 +2054,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1560
2054
|
case 4: // h → previous display
|
|
1561
2055
|
if let ed = editor, ed.displays.count > 1 {
|
|
1562
2056
|
ed.cyclePreviousDisplay()
|
|
2057
|
+
focusViewportPreset(ed.activeViewportPreset ?? .main, flashView: false)
|
|
1563
2058
|
let label = ed.focusedDisplay?.label ?? "All displays"
|
|
1564
2059
|
flash(label)
|
|
1565
2060
|
objectWillChange.send()
|
|
@@ -1569,6 +2064,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1569
2064
|
case 37: // l → next display
|
|
1570
2065
|
if let ed = editor, ed.displays.count > 1 {
|
|
1571
2066
|
ed.cycleNextDisplay()
|
|
2067
|
+
focusViewportPreset(ed.activeViewportPreset ?? .main, flashView: false)
|
|
1572
2068
|
let label = ed.focusedDisplay?.label ?? "All displays"
|
|
1573
2069
|
flash(label)
|
|
1574
2070
|
objectWillChange.send()
|
|
@@ -1603,6 +2099,22 @@ final class ScreenMapController: ObservableObject {
|
|
|
1603
2099
|
}
|
|
1604
2100
|
return true
|
|
1605
2101
|
|
|
2102
|
+
case 18: // 1 → main viewport
|
|
2103
|
+
focusViewportPreset(.main)
|
|
2104
|
+
return true
|
|
2105
|
+
|
|
2106
|
+
case 19: // 2 → top-right viewport
|
|
2107
|
+
focusViewportPreset(.topRight)
|
|
2108
|
+
return true
|
|
2109
|
+
|
|
2110
|
+
case 20: // 3 → bottom-left viewport
|
|
2111
|
+
focusViewportPreset(.bottomLeft)
|
|
2112
|
+
return true
|
|
2113
|
+
|
|
2114
|
+
case 21: // 4 → bottom-right viewport
|
|
2115
|
+
focusViewportPreset(.bottomRight)
|
|
2116
|
+
return true
|
|
2117
|
+
|
|
1606
2118
|
case 33: // [ → move to previous layer
|
|
1607
2119
|
if let ed = editor {
|
|
1608
2120
|
for wid in selectedWindowIds {
|
|
@@ -1650,15 +2162,22 @@ final class ScreenMapController: ObservableObject {
|
|
|
1650
2162
|
distributeVisible()
|
|
1651
2163
|
return true
|
|
1652
2164
|
|
|
1653
|
-
case 15: // r →
|
|
1654
|
-
|
|
1655
|
-
flash("Fit all")
|
|
2165
|
+
case 15: // r → overview
|
|
2166
|
+
focusViewportPreset(.overview)
|
|
1656
2167
|
return true
|
|
1657
2168
|
|
|
1658
2169
|
case 5: // g → grow to fill
|
|
1659
2170
|
fitAvailableSpace()
|
|
1660
2171
|
return true
|
|
1661
2172
|
|
|
2173
|
+
case 32: // u → save current selection as set
|
|
2174
|
+
createWindowSetFromSelection()
|
|
2175
|
+
return true
|
|
2176
|
+
|
|
2177
|
+
case 46: // m → materialize current viewport
|
|
2178
|
+
materializeViewport()
|
|
2179
|
+
return true
|
|
2180
|
+
|
|
1662
2181
|
case 3: // f → flatten
|
|
1663
2182
|
flattenLayers()
|
|
1664
2183
|
return true
|
|
@@ -1689,14 +2208,14 @@ final class ScreenMapController: ObservableObject {
|
|
|
1689
2208
|
}
|
|
1690
2209
|
return true
|
|
1691
2210
|
|
|
1692
|
-
case 29: // 0 →
|
|
1693
|
-
|
|
1694
|
-
flash("Fit all")
|
|
2211
|
+
case 29: // 0 → overview (secondary)
|
|
2212
|
+
focusViewportPreset(.overview)
|
|
1695
2213
|
return true
|
|
1696
2214
|
|
|
1697
2215
|
case 123: // ← previous display (secondary)
|
|
1698
2216
|
if let ed = editor, ed.displays.count > 1 {
|
|
1699
2217
|
ed.cyclePreviousDisplay()
|
|
2218
|
+
focusViewportPreset(ed.activeViewportPreset ?? .main, flashView: false)
|
|
1700
2219
|
let label = ed.focusedDisplay?.label ?? "All displays"
|
|
1701
2220
|
flash(label)
|
|
1702
2221
|
objectWillChange.send()
|
|
@@ -1706,6 +2225,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1706
2225
|
case 124: // → next display (secondary)
|
|
1707
2226
|
if let ed = editor, ed.displays.count > 1 {
|
|
1708
2227
|
ed.cycleNextDisplay()
|
|
2228
|
+
focusViewportPreset(ed.activeViewportPreset ?? .main, flashView: false)
|
|
1709
2229
|
let label = ed.focusedDisplay?.label ?? "All displays"
|
|
1710
2230
|
flash(label)
|
|
1711
2231
|
objectWillChange.send()
|
|
@@ -1729,11 +2249,11 @@ final class ScreenMapController: ObservableObject {
|
|
|
1729
2249
|
|
|
1730
2250
|
// MARK: - Actions
|
|
1731
2251
|
|
|
1732
|
-
func applyEdits() {
|
|
2252
|
+
func applyEdits(showFlash: Bool = true) {
|
|
1733
2253
|
guard let ed = editor else { return }
|
|
1734
2254
|
let pendingEdits = ed.windows.filter(\.hasEdits)
|
|
1735
2255
|
guard !pendingEdits.isEmpty else {
|
|
1736
|
-
flash("No changes to apply")
|
|
2256
|
+
if showFlash { flash("No changes to apply") }
|
|
1737
2257
|
return
|
|
1738
2258
|
}
|
|
1739
2259
|
|
|
@@ -1772,8 +2292,10 @@ final class ScreenMapController: ObservableObject {
|
|
|
1772
2292
|
actionLog.verify()
|
|
1773
2293
|
}
|
|
1774
2294
|
|
|
1775
|
-
|
|
1776
|
-
|
|
2295
|
+
if showFlash {
|
|
2296
|
+
let noun = pendingEdits.count == 1 ? "edit" : "edits"
|
|
2297
|
+
flash("Applied \(pendingEdits.count) \(noun)")
|
|
2298
|
+
}
|
|
1777
2299
|
}
|
|
1778
2300
|
|
|
1779
2301
|
func applyEditsFromButton() {
|
|
@@ -1781,6 +2303,129 @@ final class ScreenMapController: ObservableObject {
|
|
|
1781
2303
|
applyEdits()
|
|
1782
2304
|
}
|
|
1783
2305
|
|
|
2306
|
+
private func projectionTargetBounds(for editor: ScreenMapEditorState) -> CGRect {
|
|
2307
|
+
if let focused = editor.focusedDisplay {
|
|
2308
|
+
return focused.cgRect
|
|
2309
|
+
}
|
|
2310
|
+
guard let first = editor.displays.first else {
|
|
2311
|
+
return editor.viewportWorldRect
|
|
2312
|
+
}
|
|
2313
|
+
return editor.displays.dropFirst().reduce(first.cgRect) { $0.union($1.cgRect) }
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
private func parkingBounds(for editor: ScreenMapEditorState) -> CGRect {
|
|
2317
|
+
let target = projectionTargetBounds(for: editor)
|
|
2318
|
+
let union = editor.displays.dropFirst().reduce(editor.displays.first?.cgRect ?? target) { $0.union($1.cgRect) }
|
|
2319
|
+
let width: CGFloat = 420
|
|
2320
|
+
return CGRect(
|
|
2321
|
+
x: union.maxX + 120,
|
|
2322
|
+
y: union.minY + 40,
|
|
2323
|
+
width: width,
|
|
2324
|
+
height: max(union.height - 80, 320)
|
|
2325
|
+
)
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
private func projectedViewportFrames(
|
|
2329
|
+
for windows: [ScreenMapWindowEntry],
|
|
2330
|
+
source: CGRect,
|
|
2331
|
+
target: CGRect
|
|
2332
|
+
) -> [UInt32: CGRect] {
|
|
2333
|
+
guard source.width > 0, source.height > 0, target.width > 0, target.height > 0 else { return [:] }
|
|
2334
|
+
|
|
2335
|
+
let scale = min(target.width / source.width, target.height / source.height)
|
|
2336
|
+
let projectedSize = CGSize(width: source.width * scale, height: source.height * scale)
|
|
2337
|
+
let projectedOrigin = CGPoint(
|
|
2338
|
+
x: target.minX + (target.width - projectedSize.width) / 2,
|
|
2339
|
+
y: target.minY + (target.height - projectedSize.height) / 2
|
|
2340
|
+
)
|
|
2341
|
+
|
|
2342
|
+
var frames: [UInt32: CGRect] = [:]
|
|
2343
|
+
for win in windows {
|
|
2344
|
+
let frame = win.virtualFrame
|
|
2345
|
+
let mapped = CGRect(
|
|
2346
|
+
x: projectedOrigin.x + (frame.minX - source.minX) * scale,
|
|
2347
|
+
y: projectedOrigin.y + (frame.minY - source.minY) * scale,
|
|
2348
|
+
width: max(frame.width * scale, 180),
|
|
2349
|
+
height: max(frame.height * scale, 100)
|
|
2350
|
+
)
|
|
2351
|
+
frames[win.id] = mapped
|
|
2352
|
+
}
|
|
2353
|
+
return frames
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
private func parkedViewportFrames(
|
|
2357
|
+
for windows: [ScreenMapWindowEntry],
|
|
2358
|
+
parkingBounds: CGRect
|
|
2359
|
+
) -> [UInt32: CGRect] {
|
|
2360
|
+
guard !windows.isEmpty else { return [:] }
|
|
2361
|
+
|
|
2362
|
+
let spacing: CGFloat = 20
|
|
2363
|
+
let maxCardWidth = max(min(parkingBounds.width - spacing * 2, 320), 180)
|
|
2364
|
+
var cursor = CGPoint(x: parkingBounds.minX, y: parkingBounds.minY)
|
|
2365
|
+
var columnX = parkingBounds.minX
|
|
2366
|
+
var frames: [UInt32: CGRect] = [:]
|
|
2367
|
+
|
|
2368
|
+
for win in windows.sorted(by: { $0.zIndex < $1.zIndex }) {
|
|
2369
|
+
let aspect = max(win.virtualFrame.width / max(win.virtualFrame.height, 1), 0.6)
|
|
2370
|
+
let width = min(max(win.virtualFrame.width * 0.55, 180), maxCardWidth)
|
|
2371
|
+
let height = min(max(width / aspect, 100), 220)
|
|
2372
|
+
|
|
2373
|
+
if cursor.y + height > parkingBounds.maxY {
|
|
2374
|
+
columnX += maxCardWidth + spacing
|
|
2375
|
+
cursor = CGPoint(x: columnX, y: parkingBounds.minY)
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
frames[win.id] = CGRect(x: cursor.x, y: cursor.y, width: width, height: height)
|
|
2379
|
+
cursor.y += height + spacing
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
return frames
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
func materializeViewport() {
|
|
2386
|
+
guard let ed = editor else { return }
|
|
2387
|
+
|
|
2388
|
+
let scopedWindows = ed.focusedDisplayIndex != nil ? ed.focusedVisibleWindows : ed.visibleWindows
|
|
2389
|
+
guard !scopedWindows.isEmpty else {
|
|
2390
|
+
flash("No windows in scope")
|
|
2391
|
+
return
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
let viewport = ed.viewportWorldRect
|
|
2395
|
+
let projectable = scopedWindows.filter { win in
|
|
2396
|
+
win.virtualFrame.intersects(viewport) ||
|
|
2397
|
+
viewport.contains(CGPoint(x: win.virtualFrame.midX, y: win.virtualFrame.midY))
|
|
2398
|
+
}
|
|
2399
|
+
guard !projectable.isEmpty else {
|
|
2400
|
+
flash("Viewport is empty")
|
|
2401
|
+
return
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
let targetBounds = projectionTargetBounds(for: ed)
|
|
2405
|
+
let parkingBounds = parkingBounds(for: ed)
|
|
2406
|
+
let projectedFrames = projectedViewportFrames(
|
|
2407
|
+
for: projectable,
|
|
2408
|
+
source: viewport,
|
|
2409
|
+
target: targetBounds
|
|
2410
|
+
)
|
|
2411
|
+
let parkedWindows = scopedWindows.filter { projectedFrames[$0.id] == nil }
|
|
2412
|
+
let parkedFrames = parkedViewportFrames(for: parkedWindows, parkingBounds: parkingBounds)
|
|
2413
|
+
|
|
2414
|
+
for idx in ed.windows.indices {
|
|
2415
|
+
let wid = ed.windows[idx].id
|
|
2416
|
+
if let frame = projectedFrames[wid] ?? parkedFrames[wid] {
|
|
2417
|
+
ed.windows[idx].editedFrame = frame
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
DiagnosticLog.shared.info(
|
|
2422
|
+
"[Canvas] materialize → projected \(projectedFrames.count), parked \(parkedFrames.count), scope=\(ed.canvasScopeSummary)"
|
|
2423
|
+
)
|
|
2424
|
+
if ed.isPreviewing { endPreview() }
|
|
2425
|
+
applyEdits(showFlash: false)
|
|
2426
|
+
flash("Projected \(projectedFrames.count) windows")
|
|
2427
|
+
}
|
|
2428
|
+
|
|
1784
2429
|
func exitScreenMap() {
|
|
1785
2430
|
if editor?.isPreviewing == true { endPreview() }
|
|
1786
2431
|
WindowBezel.shared.dismiss()
|
|
@@ -1906,7 +2551,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1906
2551
|
let idx = ed.windows.firstIndex(where: { $0.id == winId }),
|
|
1907
2552
|
let display = ed.displays.first(where: { $0.index == ed.windows[idx].displayIndex })
|
|
1908
2553
|
else { return }
|
|
1909
|
-
ed.
|
|
2554
|
+
ed.syncLayoutFrame(at: idx, to: WindowTiler.tileFrame(for: position, inDisplay: display.cgRect))
|
|
1910
2555
|
ed.isTilingMode = false
|
|
1911
2556
|
ed.objectWillChange.send(); objectWillChange.send()
|
|
1912
2557
|
flash(position.label)
|
|
@@ -1918,7 +2563,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1918
2563
|
let idx = ed.windows.firstIndex(where: { $0.id == winId }),
|
|
1919
2564
|
let display = ed.displays.first(where: { $0.index == ed.windows[idx].displayIndex })
|
|
1920
2565
|
else { return }
|
|
1921
|
-
ed.
|
|
2566
|
+
ed.syncLayoutFrame(at: idx, to: WindowTiler.tileFrame(fractions: fractions, inDisplay: display.cgRect))
|
|
1922
2567
|
ed.isTilingMode = false
|
|
1923
2568
|
ed.objectWillChange.send(); objectWillChange.send()
|
|
1924
2569
|
flash(label)
|
|
@@ -1962,7 +2607,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1962
2607
|
display = ed.displays.first(where: { $0.index == ed.windows[idx].displayIndex }) ?? ed.displays[0]
|
|
1963
2608
|
}
|
|
1964
2609
|
|
|
1965
|
-
ed.
|
|
2610
|
+
ed.syncLayoutFrame(at: idx, to: WindowTiler.tileFrame(fractions: fractions, inDisplay: display.cgRect))
|
|
1966
2611
|
// Update display index if layout spec moves to a different display
|
|
1967
2612
|
if let spatialNum = spec.display {
|
|
1968
2613
|
let order = ed.spatialDisplayOrder
|
|
@@ -1973,6 +2618,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1973
2618
|
app: moved.app, title: moved.title,
|
|
1974
2619
|
originalFrame: moved.originalFrame,
|
|
1975
2620
|
editedFrame: moved.editedFrame,
|
|
2621
|
+
virtualFrame: moved.virtualFrame,
|
|
1976
2622
|
zIndex: moved.zIndex, layer: moved.layer,
|
|
1977
2623
|
displayIndex: order[spatialNum - 1].index,
|
|
1978
2624
|
isOnScreen: moved.isOnScreen,
|
|
@@ -2017,7 +2663,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
2017
2663
|
[.boundsIgnoreFraming, .bestResolution]
|
|
2018
2664
|
) {
|
|
2019
2665
|
captures[win.id] = NSImage(cgImage: cgImage,
|
|
2020
|
-
size: NSSize(width: win.
|
|
2666
|
+
size: NSSize(width: win.virtualFrame.width, height: win.virtualFrame.height))
|
|
2021
2667
|
}
|
|
2022
2668
|
}
|
|
2023
2669
|
previewCaptures = captures
|