@lattices/cli 0.4.8 → 0.4.10
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/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Sources/AppShell/AppDelegate.swift +4 -0
- package/app/Sources/AppShell/AppUpdater.swift +216 -4
- package/app/Sources/AppShell/SettingsView.swift +78 -3
- package/app/Sources/Core/Desktop/DesktopModel.swift +1 -0
- package/app/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +38 -19
- package/app/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
- package/package.json +1 -1
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
</dict>
|
|
27
27
|
</array>
|
|
28
28
|
<key>CFBundleVersion</key>
|
|
29
|
-
<string>0.4.
|
|
29
|
+
<string>0.4.10</string>
|
|
30
30
|
<key>CFBundleShortVersionString</key>
|
|
31
|
-
<string>0.4.
|
|
31
|
+
<string>0.4.10</string>
|
|
32
32
|
<key>LSMinimumSystemVersion</key>
|
|
33
33
|
<string>13.0</string>
|
|
34
34
|
<key>LSUIElement</key>
|
|
Binary file
|
|
@@ -194,6 +194,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
194
194
|
AgentPool.shared.start()
|
|
195
195
|
diag.finish(tBoot)
|
|
196
196
|
|
|
197
|
+
Task {
|
|
198
|
+
await AppUpdater.shared.checkIfNeeded()
|
|
199
|
+
}
|
|
200
|
+
|
|
197
201
|
// --diagnostics flag: auto-open diagnostics panel on launch
|
|
198
202
|
if CommandLine.arguments.contains("--diagnostics") {
|
|
199
203
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import Combine
|
|
3
3
|
import Foundation
|
|
4
|
+
import SwiftUI
|
|
5
|
+
|
|
6
|
+
struct LatticesUpdateInfo: Equatable {
|
|
7
|
+
let version: String
|
|
8
|
+
let downloadURL: URL
|
|
9
|
+
let releaseNotes: String
|
|
10
|
+
let publishedAt: Date
|
|
11
|
+
let htmlURL: URL
|
|
12
|
+
}
|
|
4
13
|
|
|
5
14
|
@MainActor
|
|
6
15
|
final class AppUpdater: ObservableObject {
|
|
@@ -8,6 +17,16 @@ final class AppUpdater: ObservableObject {
|
|
|
8
17
|
|
|
9
18
|
@Published private(set) var isUpdating = false
|
|
10
19
|
@Published private(set) var statusMessage: String?
|
|
20
|
+
@Published private(set) var availableUpdate: LatticesUpdateInfo?
|
|
21
|
+
@Published private(set) var isChecking = false
|
|
22
|
+
@Published private(set) var lastChecked: Date?
|
|
23
|
+
@Published private(set) var lastError: String?
|
|
24
|
+
|
|
25
|
+
@AppStorage("appUpdater.autoCheck") var autoCheckEnabled = true
|
|
26
|
+
@AppStorage("appUpdater.lastCheckTime") private var lastCheckTimeInterval: Double = 0
|
|
27
|
+
@AppStorage("appUpdater.skippedVersion") private var skippedVersion = ""
|
|
28
|
+
|
|
29
|
+
private let checkInterval: TimeInterval = 24 * 60 * 60
|
|
11
30
|
|
|
12
31
|
private init() {}
|
|
13
32
|
|
|
@@ -27,6 +46,62 @@ final class AppUpdater: ObservableObject {
|
|
|
27
46
|
return nil
|
|
28
47
|
}
|
|
29
48
|
|
|
49
|
+
func checkIfNeeded() async {
|
|
50
|
+
guard autoCheckEnabled else { return }
|
|
51
|
+
|
|
52
|
+
let now = Date()
|
|
53
|
+
let lastCheck = Date(timeIntervalSince1970: lastCheckTimeInterval)
|
|
54
|
+
if now.timeIntervalSince(lastCheck) < checkInterval { return }
|
|
55
|
+
|
|
56
|
+
await check()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func check() async {
|
|
60
|
+
guard !isChecking else { return }
|
|
61
|
+
|
|
62
|
+
isChecking = true
|
|
63
|
+
lastError = nil
|
|
64
|
+
|
|
65
|
+
defer {
|
|
66
|
+
isChecking = false
|
|
67
|
+
lastChecked = Date()
|
|
68
|
+
lastCheckTimeInterval = Date().timeIntervalSince1970
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
do {
|
|
72
|
+
let release = try await fetchLatestRelease()
|
|
73
|
+
guard let update = parseRelease(release), isNewerVersion(update.version) else {
|
|
74
|
+
availableUpdate = nil
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if update.version == skippedVersion {
|
|
79
|
+
availableUpdate = nil
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
availableUpdate = update
|
|
84
|
+
} catch UpdateCheckError.noRelease {
|
|
85
|
+
availableUpdate = nil
|
|
86
|
+
} catch {
|
|
87
|
+
lastError = error.localizedDescription
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func skipCurrentUpdate() {
|
|
92
|
+
guard let update = availableUpdate else { return }
|
|
93
|
+
skippedVersion = update.version
|
|
94
|
+
availableUpdate = nil
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func viewCurrentRelease() {
|
|
98
|
+
if let update = availableUpdate {
|
|
99
|
+
NSWorkspace.shared.open(update.htmlURL)
|
|
100
|
+
} else if let url = URL(string: "https://github.com/arach/lattices/releases/latest") {
|
|
101
|
+
NSWorkspace.shared.open(url)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
30
105
|
func promptForUpdate() {
|
|
31
106
|
guard canUpdate else {
|
|
32
107
|
presentAlert(
|
|
@@ -36,11 +111,50 @@ final class AppUpdater: ObservableObject {
|
|
|
36
111
|
return
|
|
37
112
|
}
|
|
38
113
|
|
|
114
|
+
guard availableUpdate != nil else {
|
|
115
|
+
Task {
|
|
116
|
+
await check()
|
|
117
|
+
if availableUpdate != nil {
|
|
118
|
+
presentUpdateConfirmation()
|
|
119
|
+
} else if let error = lastError {
|
|
120
|
+
presentAlert(
|
|
121
|
+
title: "Could Not Check for Updates",
|
|
122
|
+
message: error
|
|
123
|
+
)
|
|
124
|
+
} else {
|
|
125
|
+
presentAlert(
|
|
126
|
+
title: "Lattices Is Up to Date",
|
|
127
|
+
message: "You’re running \(currentVersion), which is the latest published release."
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
presentUpdateConfirmation()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func presentUpdateConfirmation() {
|
|
39
138
|
let alert = NSAlert()
|
|
40
139
|
alert.alertStyle = .informational
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
140
|
+
if let update = availableUpdate {
|
|
141
|
+
alert.messageText = "Update Lattices?"
|
|
142
|
+
alert.informativeText = """
|
|
143
|
+
Current version: \(currentVersion)
|
|
144
|
+
New version: \(update.version)
|
|
145
|
+
|
|
146
|
+
Lattices will download the signed release, quit briefly, replace the app, and relaunch when the update is ready.
|
|
147
|
+
"""
|
|
148
|
+
} else {
|
|
149
|
+
alert.messageText = "Check and update Lattices?"
|
|
150
|
+
alert.informativeText = """
|
|
151
|
+
Current version: \(currentVersion)
|
|
152
|
+
New version: latest published release
|
|
153
|
+
|
|
154
|
+
Lattices will download the signed release, quit briefly, replace the app, and relaunch when the update is ready.
|
|
155
|
+
"""
|
|
156
|
+
}
|
|
157
|
+
alert.addButton(withTitle: availableUpdate == nil ? "Check & Update" : "Update")
|
|
44
158
|
alert.addButton(withTitle: "Cancel")
|
|
45
159
|
|
|
46
160
|
guard alert.runModal() == .alertFirstButtonReturn else { return }
|
|
@@ -72,7 +186,11 @@ final class AppUpdater: ObservableObject {
|
|
|
72
186
|
do {
|
|
73
187
|
try proc.run()
|
|
74
188
|
isUpdating = true
|
|
75
|
-
|
|
189
|
+
if let update = availableUpdate {
|
|
190
|
+
statusMessage = "Preparing Lattices \(update.version). The app will relaunch when the update is ready."
|
|
191
|
+
} else {
|
|
192
|
+
statusMessage = "Preparing the latest Lattices release. The app will relaunch when the update is ready."
|
|
193
|
+
}
|
|
76
194
|
} catch {
|
|
77
195
|
presentAlert(
|
|
78
196
|
title: "Update Failed",
|
|
@@ -89,4 +207,98 @@ final class AppUpdater: ObservableObject {
|
|
|
89
207
|
alert.addButton(withTitle: "OK")
|
|
90
208
|
alert.runModal()
|
|
91
209
|
}
|
|
210
|
+
|
|
211
|
+
private func fetchLatestRelease() async throws -> GitHubRelease {
|
|
212
|
+
let url = URL(string: "https://api.github.com/repos/arach/lattices/releases/latest")!
|
|
213
|
+
var request = URLRequest(url: url)
|
|
214
|
+
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
|
|
215
|
+
request.setValue("Lattices/\(currentVersion)", forHTTPHeaderField: "User-Agent")
|
|
216
|
+
|
|
217
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
218
|
+
guard let http = response as? HTTPURLResponse else {
|
|
219
|
+
throw UpdateCheckError.invalidResponse
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
switch http.statusCode {
|
|
223
|
+
case 200:
|
|
224
|
+
let decoder = JSONDecoder()
|
|
225
|
+
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
226
|
+
decoder.dateDecodingStrategy = .iso8601
|
|
227
|
+
return try decoder.decode(GitHubRelease.self, from: data)
|
|
228
|
+
case 404:
|
|
229
|
+
throw UpdateCheckError.noRelease
|
|
230
|
+
case 403:
|
|
231
|
+
throw UpdateCheckError.rateLimited
|
|
232
|
+
default:
|
|
233
|
+
throw UpdateCheckError.httpError(http.statusCode)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private func parseRelease(_ release: GitHubRelease) -> LatticesUpdateInfo? {
|
|
238
|
+
guard !release.draft, !release.prerelease else { return nil }
|
|
239
|
+
|
|
240
|
+
let asset = release.assets.first { asset in
|
|
241
|
+
asset.name == "Lattices.dmg" ||
|
|
242
|
+
(asset.name.hasPrefix("Lattices") && asset.name.hasSuffix(".dmg"))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
guard let asset,
|
|
246
|
+
let downloadURL = URL(string: asset.browserDownloadUrl),
|
|
247
|
+
let htmlURL = URL(string: release.htmlUrl) else {
|
|
248
|
+
return nil
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let version = release.tagName.hasPrefix("v")
|
|
252
|
+
? String(release.tagName.dropFirst())
|
|
253
|
+
: release.tagName
|
|
254
|
+
|
|
255
|
+
return LatticesUpdateInfo(
|
|
256
|
+
version: version,
|
|
257
|
+
downloadURL: downloadURL,
|
|
258
|
+
releaseNotes: release.body ?? "",
|
|
259
|
+
publishedAt: release.publishedAt,
|
|
260
|
+
htmlURL: htmlURL
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private func isNewerVersion(_ remoteVersion: String) -> Bool {
|
|
265
|
+
guard currentVersion != "unknown" else { return false }
|
|
266
|
+
return remoteVersion.compare(currentVersion, options: .numeric) == .orderedDescending
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private struct GitHubRelease: Decodable {
|
|
271
|
+
let tagName: String
|
|
272
|
+
let name: String
|
|
273
|
+
let body: String?
|
|
274
|
+
let htmlUrl: String
|
|
275
|
+
let publishedAt: Date
|
|
276
|
+
let assets: [GitHubAsset]
|
|
277
|
+
let prerelease: Bool
|
|
278
|
+
let draft: Bool
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private struct GitHubAsset: Decodable {
|
|
282
|
+
let name: String
|
|
283
|
+
let browserDownloadUrl: String
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private enum UpdateCheckError: LocalizedError {
|
|
287
|
+
case invalidResponse
|
|
288
|
+
case noRelease
|
|
289
|
+
case rateLimited
|
|
290
|
+
case httpError(Int)
|
|
291
|
+
|
|
292
|
+
var errorDescription: String? {
|
|
293
|
+
switch self {
|
|
294
|
+
case .invalidResponse:
|
|
295
|
+
return "Invalid response from GitHub."
|
|
296
|
+
case .noRelease:
|
|
297
|
+
return "No published release found."
|
|
298
|
+
case .rateLimited:
|
|
299
|
+
return "GitHub rate limited the update check."
|
|
300
|
+
case .httpError(let code):
|
|
301
|
+
return "GitHub returned HTTP \(code)."
|
|
302
|
+
}
|
|
303
|
+
}
|
|
92
304
|
}
|
|
@@ -301,15 +301,53 @@ struct SettingsContentView: View {
|
|
|
301
301
|
.font(Typo.mono(12))
|
|
302
302
|
.foregroundColor(Palette.text)
|
|
303
303
|
Spacer()
|
|
304
|
-
Text("v\(appUpdater.currentVersion)")
|
|
304
|
+
Text("Current v\(appUpdater.currentVersion)")
|
|
305
305
|
.font(Typo.caption(10))
|
|
306
306
|
.foregroundColor(Palette.textMuted)
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
Text("
|
|
309
|
+
Text("Lattices can check for new signed releases and prepare the update here. You’ll confirm before the app quits and relaunches.")
|
|
310
310
|
.font(Typo.caption(10))
|
|
311
311
|
.foregroundColor(Palette.textMuted)
|
|
312
312
|
|
|
313
|
+
if let update = appUpdater.availableUpdate {
|
|
314
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
315
|
+
HStack(spacing: 6) {
|
|
316
|
+
Image(systemName: "gift.fill")
|
|
317
|
+
.font(.system(size: 10, weight: .semibold))
|
|
318
|
+
.foregroundColor(Palette.detach)
|
|
319
|
+
Text("New version v\(update.version) is ready")
|
|
320
|
+
.font(Typo.monoBold(10))
|
|
321
|
+
.foregroundColor(Palette.detach)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if !update.releaseNotes.isEmpty {
|
|
325
|
+
Text(String(update.releaseNotes.prefix(180)) + (update.releaseNotes.count > 180 ? "..." : ""))
|
|
326
|
+
.font(Typo.caption(9))
|
|
327
|
+
.foregroundColor(Palette.textMuted)
|
|
328
|
+
.lineLimit(3)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
.padding(8)
|
|
332
|
+
.background(
|
|
333
|
+
RoundedRectangle(cornerRadius: 5)
|
|
334
|
+
.fill(Palette.surfaceHov.opacity(0.65))
|
|
335
|
+
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.detach.opacity(0.35), lineWidth: 0.5))
|
|
336
|
+
)
|
|
337
|
+
} else if appUpdater.isChecking {
|
|
338
|
+
Text("Checking for updates...")
|
|
339
|
+
.font(Typo.caption(9))
|
|
340
|
+
.foregroundColor(Palette.textMuted)
|
|
341
|
+
} else if let error = appUpdater.lastError {
|
|
342
|
+
Text(error)
|
|
343
|
+
.font(Typo.caption(9))
|
|
344
|
+
.foregroundColor(Palette.detach.opacity(0.9))
|
|
345
|
+
} else if let checked = appUpdater.lastChecked {
|
|
346
|
+
Text("Last checked \(checked, style: .relative)")
|
|
347
|
+
.font(Typo.caption(9))
|
|
348
|
+
.foregroundColor(Palette.textMuted.opacity(0.8))
|
|
349
|
+
}
|
|
350
|
+
|
|
313
351
|
if let status = appUpdater.statusMessage {
|
|
314
352
|
Text(status)
|
|
315
353
|
.font(Typo.caption(9))
|
|
@@ -326,7 +364,7 @@ struct SettingsContentView: View {
|
|
|
326
364
|
Button {
|
|
327
365
|
appUpdater.promptForUpdate()
|
|
328
366
|
} label: {
|
|
329
|
-
Text(appUpdater.isUpdating ? "
|
|
367
|
+
Text(appUpdater.isUpdating ? "Preparing…" : (appUpdater.availableUpdate == nil ? "Check for Updates" : "Update to v\(appUpdater.availableUpdate?.version ?? "")"))
|
|
330
368
|
.font(Typo.monoBold(10))
|
|
331
369
|
.foregroundColor(Palette.text)
|
|
332
370
|
.padding(.horizontal, 12)
|
|
@@ -340,6 +378,43 @@ struct SettingsContentView: View {
|
|
|
340
378
|
.buttonStyle(.plain)
|
|
341
379
|
.disabled(appUpdater.isUpdating)
|
|
342
380
|
|
|
381
|
+
Button {
|
|
382
|
+
Task { await appUpdater.check() }
|
|
383
|
+
} label: {
|
|
384
|
+
Text(appUpdater.isChecking ? "Checking..." : "Check Now")
|
|
385
|
+
.font(Typo.caption(9))
|
|
386
|
+
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
387
|
+
}
|
|
388
|
+
.buttonStyle(.plain)
|
|
389
|
+
.disabled(appUpdater.isChecking)
|
|
390
|
+
|
|
391
|
+
Toggle("Auto", isOn: $appUpdater.autoCheckEnabled)
|
|
392
|
+
.font(Typo.caption(9))
|
|
393
|
+
.toggleStyle(.checkbox)
|
|
394
|
+
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
395
|
+
|
|
396
|
+
if appUpdater.availableUpdate != nil {
|
|
397
|
+
Button {
|
|
398
|
+
appUpdater.viewCurrentRelease()
|
|
399
|
+
} label: {
|
|
400
|
+
Text("Release Notes")
|
|
401
|
+
.font(Typo.caption(9))
|
|
402
|
+
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
403
|
+
}
|
|
404
|
+
.buttonStyle(.plain)
|
|
405
|
+
|
|
406
|
+
Button {
|
|
407
|
+
appUpdater.skipCurrentUpdate()
|
|
408
|
+
} label: {
|
|
409
|
+
Text("Skip")
|
|
410
|
+
.font(Typo.caption(9))
|
|
411
|
+
.foregroundColor(Palette.textMuted.opacity(0.75))
|
|
412
|
+
}
|
|
413
|
+
.buttonStyle(.plain)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
Spacer()
|
|
417
|
+
|
|
343
418
|
Text("CLI: `lattices app update`")
|
|
344
419
|
.font(Typo.caption(9))
|
|
345
420
|
.foregroundColor(Palette.textMuted.opacity(0.8))
|
|
@@ -144,7 +144,7 @@ enum CanvasDragMode {
|
|
|
144
144
|
|
|
145
145
|
final class ScreenMapEditorState: ObservableObject {
|
|
146
146
|
@Published var windows: [ScreenMapWindowEntry]
|
|
147
|
-
@Published var selectedLayers: Set<Int> = [
|
|
147
|
+
@Published var selectedLayers: Set<Int> = [] // empty = show all
|
|
148
148
|
@Published var draggingWindowId: UInt32? = nil
|
|
149
149
|
var canvasDragMode: CanvasDragMode = .move
|
|
150
150
|
var currentCursorMode: CanvasDragMode = .move
|
|
@@ -153,7 +153,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
153
153
|
@Published var zoomLevel: CGFloat = 1.0 // 1.0 = fit-all
|
|
154
154
|
@Published var panOffset: CGPoint = .zero // canvas-local pixels
|
|
155
155
|
@Published var focusedDisplayIndex: Int? = nil // nil = all-displays view
|
|
156
|
-
@Published var activeViewportPreset: ScreenMapViewportPreset? = .
|
|
156
|
+
@Published var activeViewportPreset: ScreenMapViewportPreset? = .overview
|
|
157
157
|
@Published var windowSearchQuery: String = ""
|
|
158
158
|
@Published var isTilingMode: Bool = false
|
|
159
159
|
var isSearching: Bool { !windowSearchQuery.isEmpty }
|
|
@@ -380,10 +380,11 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
380
380
|
return "L\(layer)"
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
/// Windows visible for the active layer filter
|
|
383
|
+
/// Windows visible on the current desktop for the active layer filter.
|
|
384
384
|
var visibleWindows: [ScreenMapWindowEntry] {
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
let onscreen = windows.filter(\.isOnScreen)
|
|
386
|
+
guard !selectedLayers.isEmpty else { return onscreen }
|
|
387
|
+
return onscreen.filter { selectedLayers.contains($0.layer) }
|
|
387
388
|
}
|
|
388
389
|
|
|
389
390
|
private var worldScopedDisplays: [DisplayGeometry] {
|
|
@@ -398,7 +399,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
398
399
|
|
|
399
400
|
var canvasWorldBounds: CGRect {
|
|
400
401
|
var rects = worldScopedDisplays.map(\.cgRect)
|
|
401
|
-
rects.append(contentsOf: worldScopedWindows.map(\.virtualFrame))
|
|
402
|
+
rects.append(contentsOf: worldScopedWindows.filter(\.hasEdits).map(\.virtualFrame))
|
|
402
403
|
|
|
403
404
|
if rects.isEmpty {
|
|
404
405
|
return CGRect(origin: bboxOrigin, size: screenSize)
|
|
@@ -409,7 +410,7 @@ final class ScreenMapEditorState: ObservableObject {
|
|
|
409
410
|
union = union.union(rect)
|
|
410
411
|
}
|
|
411
412
|
|
|
412
|
-
let pad: CGFloat = focusedDisplayIndex == nil ?
|
|
413
|
+
let pad: CGFloat = focusedDisplayIndex == nil ? 180 : 120
|
|
413
414
|
return union.insetBy(dx: -pad, dy: -pad)
|
|
414
415
|
}
|
|
415
416
|
|
|
@@ -1424,7 +1425,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1424
1425
|
|
|
1425
1426
|
private func finalizeDisplayFocusChange(flashLabel: Bool) {
|
|
1426
1427
|
guard let ed = editor else { return }
|
|
1427
|
-
focusViewportPreset(ed.activeViewportPreset ?? .
|
|
1428
|
+
focusViewportPreset(ed.activeViewportPreset ?? .overview, flashView: false)
|
|
1428
1429
|
if flashLabel {
|
|
1429
1430
|
flash(ed.focusedDisplay?.label ?? "All displays")
|
|
1430
1431
|
}
|
|
@@ -1454,6 +1455,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1454
1455
|
guard newZoom != ed.zoomLevel else { return }
|
|
1455
1456
|
ed.activeViewportPreset = nil
|
|
1456
1457
|
ed.zoomLevel = newZoom
|
|
1458
|
+
ed.scale = ed.fitScale * newZoom
|
|
1457
1459
|
objectWillChange.send()
|
|
1458
1460
|
}
|
|
1459
1461
|
|
|
@@ -1677,7 +1679,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1677
1679
|
func enter() {
|
|
1678
1680
|
let existingSets = windowSets
|
|
1679
1681
|
guard let windowList = CGWindowListCopyWindowInfo(
|
|
1680
|
-
[.
|
|
1682
|
+
[.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID
|
|
1681
1683
|
) as? [[String: Any]] else { return }
|
|
1682
1684
|
|
|
1683
1685
|
struct CGWin {
|
|
@@ -1724,7 +1726,7 @@ final class ScreenMapController: ObservableObject {
|
|
|
1724
1726
|
guard CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) else { continue }
|
|
1725
1727
|
guard rect.width >= 100 && rect.height >= 50 else { continue }
|
|
1726
1728
|
let app = info[kCGWindowOwnerName as String] as? String ?? ""
|
|
1727
|
-
if app == "Lattices" || app == "lattices" || app == "AutoFill" { continue }
|
|
1729
|
+
if app == "Lattices" || app == "lattices" || app == "AutoFill" || app == "Codex Computer Use" { continue }
|
|
1728
1730
|
let pid = info[kCGWindowOwnerPID as String] as? Int32 ?? 0
|
|
1729
1731
|
let title = info[kCGWindowName as String] as? String ?? ""
|
|
1730
1732
|
let dIdx = displayIndex(for: rect)
|
|
@@ -1844,15 +1846,14 @@ final class ScreenMapController: ObservableObject {
|
|
|
1844
1846
|
}
|
|
1845
1847
|
}
|
|
1846
1848
|
|
|
1847
|
-
//
|
|
1848
|
-
if
|
|
1849
|
+
// Start monitor-first: focus the display under the cursor, or the first display.
|
|
1850
|
+
if !displayGeometries.isEmpty {
|
|
1849
1851
|
let mouseLocation = NSEvent.mouseLocation
|
|
1850
1852
|
let mouseCG = CGPoint(x: mouseLocation.x, y: primaryHeight - mouseLocation.y)
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
}
|
|
1853
|
+
if let display = displayGeometries.first(where: { $0.cgRect.contains(mouseCG) }) {
|
|
1854
|
+
newEditor.focusedDisplayIndex = display.index
|
|
1855
|
+
} else {
|
|
1856
|
+
newEditor.focusedDisplayIndex = displayGeometries[0].index
|
|
1856
1857
|
}
|
|
1857
1858
|
}
|
|
1858
1859
|
|
|
@@ -1867,7 +1868,12 @@ final class ScreenMapController: ObservableObject {
|
|
|
1867
1868
|
self.activeWindowSetID = nil
|
|
1868
1869
|
}
|
|
1869
1870
|
selectedWindowIds = []
|
|
1870
|
-
focusViewportPreset(.
|
|
1871
|
+
focusViewportPreset(.overview, flashView: false)
|
|
1872
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { [weak self] in
|
|
1873
|
+
guard let self,
|
|
1874
|
+
self.editor?.activeViewportPreset == .overview else { return }
|
|
1875
|
+
self.focusViewportPreset(.overview, flashView: false)
|
|
1876
|
+
}
|
|
1871
1877
|
}
|
|
1872
1878
|
|
|
1873
1879
|
/// Re-snapshot, preserving display/layer context
|
|
@@ -1914,10 +1920,15 @@ final class ScreenMapController: ObservableObject {
|
|
|
1914
1920
|
ed.activeViewportPreset = preset
|
|
1915
1921
|
let rect = ed.viewportRect(for: preset)
|
|
1916
1922
|
DiagnosticLog.shared.info("[Canvas] preset → \(preset.title)")
|
|
1923
|
+
if preset == .overview {
|
|
1924
|
+
ed.zoomLevel = 1
|
|
1925
|
+
ed.scale = ed.fitScale
|
|
1926
|
+
ed.panOffset = .zero
|
|
1927
|
+
}
|
|
1917
1928
|
queueCanvasNavigation(
|
|
1918
1929
|
centeredOn: CGPoint(x: rect.midX, y: rect.midY),
|
|
1919
1930
|
rect: rect,
|
|
1920
|
-
zoomToFit:
|
|
1931
|
+
zoomToFit: preset != .overview
|
|
1921
1932
|
)
|
|
1922
1933
|
if flashView {
|
|
1923
1934
|
flash(preset.title)
|
|
@@ -1960,6 +1971,14 @@ final class ScreenMapController: ObservableObject {
|
|
|
1960
1971
|
}
|
|
1961
1972
|
|
|
1962
1973
|
setViewport(centeredOn: target.center, zoomLevel: targetZoom)
|
|
1974
|
+
if target.zoomToFit {
|
|
1975
|
+
ed.pendingCanvasNavigation = ScreenMapCanvasNavigationTarget(
|
|
1976
|
+
center: target.center,
|
|
1977
|
+
rect: nil,
|
|
1978
|
+
zoomToFit: false
|
|
1979
|
+
)
|
|
1980
|
+
ed.canvasNavigationRevision &+= 1
|
|
1981
|
+
}
|
|
1963
1982
|
if shouldLogView {
|
|
1964
1983
|
let viewport = ed.viewportWorldRect
|
|
1965
1984
|
DiagnosticLog.shared.info(
|