node-mac-recorder 2.24.1 → 2.24.3
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/index.js +81 -13
- package/package.json +1 -1
- package/src/cursor_tracker.mm +72 -21
- package/src/mac_recorder.mm +14 -0
- package/src/screen_capture_kit.h +5 -0
- package/src/screen_capture_kit.mm +19 -0
- package/src/window_selector.mm +50 -31
package/index.js
CHANGED
|
@@ -150,6 +150,23 @@ class MacRecorder extends EventEmitter {
|
|
|
150
150
|
/**
|
|
151
151
|
* macOS ekranlarını listeler
|
|
152
152
|
*/
|
|
153
|
+
/**
|
|
154
|
+
* ScreenCaptureKit'i onceden isitir (SCShareableContent cache).
|
|
155
|
+
* Kayit baslatmadan once cagrilirsa "start recorder" gecikmesi belirgin azalir.
|
|
156
|
+
* Asenkron ve guvenli: desteklenmeyen surumlerde sessizce false doner.
|
|
157
|
+
*/
|
|
158
|
+
prewarmScreenCapture() {
|
|
159
|
+
try {
|
|
160
|
+
if (typeof nativeBinding.prewarmScreenCapture !== "function") {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return nativeBinding.prewarmScreenCapture();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.warn("Prewarm basarisiz:", error.message);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
153
170
|
async getDisplays() {
|
|
154
171
|
const displays = nativeBinding.getDisplays();
|
|
155
172
|
return displays.map((display, index) => ({
|
|
@@ -874,6 +891,8 @@ class MacRecorder extends EventEmitter {
|
|
|
874
891
|
// bir zaman kaymasi olarak kalir. Native gercek video baslangicini
|
|
875
892
|
// biliyor; okuyup sakla ki stop'ta cursor JSON'una yazabilelim.
|
|
876
893
|
this.videoStartTimestamp = 0;
|
|
894
|
+
// Onceki kayittan kalan referans sizmasin
|
|
895
|
+
this.timelineStartTimestamp = 0;
|
|
877
896
|
try {
|
|
878
897
|
const nativeVideoStart =
|
|
879
898
|
typeof nativeBinding.getVideoStartTimestamp === 'function'
|
|
@@ -891,6 +910,28 @@ class MacRecorder extends EventEmitter {
|
|
|
891
910
|
console.warn('⚠️ SYNC: Video start timestamp read failed:', videoStartError.message);
|
|
892
911
|
}
|
|
893
912
|
|
|
913
|
+
// ZAMAN REFERANSI: cursor/klavye timeline'i VIDEONUN ilk karesine
|
|
914
|
+
// hizalanir, JS'in "kayit hazir" dedigi ana degil.
|
|
915
|
+
//
|
|
916
|
+
// NEDEN: syncTimestamp, yukaridaki hazir-olma dongusunden sonraki an.
|
|
917
|
+
// Bu an ile videonun gercek t=0'i arasindaki fark, baslatma hizina
|
|
918
|
+
// gore DEGISIYOR (ornegin ScreenCaptureKit isitilmissa video cok daha
|
|
919
|
+
// erken basliyor). Cursor bu degisken ana baglanirsa her kayitta
|
|
920
|
+
// farkli bir kayma olusuyor ve editordeki telafi 1sn limitine
|
|
921
|
+
// takilabiliyor. Videonun kendi baslangicini referans alinca fark
|
|
922
|
+
// yapisal olarak sifir olur; hizlanma senkronu bozmaz.
|
|
923
|
+
const timelineStartTimestamp =
|
|
924
|
+
this.videoStartTimestamp > 0
|
|
925
|
+
? this.videoStartTimestamp
|
|
926
|
+
: syncTimestamp;
|
|
927
|
+
this.timelineStartTimestamp = timelineStartTimestamp;
|
|
928
|
+
|
|
929
|
+
if (timelineStartTimestamp !== syncTimestamp) {
|
|
930
|
+
console.log(
|
|
931
|
+
`🎯 SYNC: Timeline referansi video ilk karesine cekildi (${(syncTimestamp - timelineStartTimestamp).toFixed(0)}ms geri)`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
894
935
|
const standardCursorOptions = {
|
|
895
936
|
videoRelative: true,
|
|
896
937
|
displayInfo: this.recordingDisplayInfo,
|
|
@@ -898,11 +939,11 @@ class MacRecorder extends EventEmitter {
|
|
|
898
939
|
this.options.captureArea ? 'area' : 'display',
|
|
899
940
|
captureArea: this.options.captureArea,
|
|
900
941
|
windowId: this.options.windowId,
|
|
901
|
-
startTimestamp:
|
|
942
|
+
startTimestamp: timelineStartTimestamp
|
|
902
943
|
};
|
|
903
944
|
|
|
904
945
|
try {
|
|
905
|
-
console.log('🎯 SYNC: Starting cursor tracking at timestamp:',
|
|
946
|
+
console.log('🎯 SYNC: Starting cursor tracking at timestamp:', timelineStartTimestamp);
|
|
906
947
|
await this.startCursorCapture(cursorFilePath, standardCursorOptions);
|
|
907
948
|
console.log('✅ SYNC: Cursor tracking started successfully');
|
|
908
949
|
} catch (cursorError) {
|
|
@@ -912,8 +953,8 @@ class MacRecorder extends EventEmitter {
|
|
|
912
953
|
|
|
913
954
|
// Klavye kısayolu yakalama (cursor ile AYNI zaman referansı)
|
|
914
955
|
try {
|
|
915
|
-
console.log('⌨️ SYNC: Starting keyboard shortcut capture at timestamp:',
|
|
916
|
-
await this.startKeyboardCapture(keyboardFilePath, { startTimestamp:
|
|
956
|
+
console.log('⌨️ SYNC: Starting keyboard shortcut capture at timestamp:', timelineStartTimestamp);
|
|
957
|
+
await this.startKeyboardCapture(keyboardFilePath, { startTimestamp: timelineStartTimestamp });
|
|
917
958
|
console.log('✅ SYNC: Keyboard shortcut capture started successfully');
|
|
918
959
|
} catch (keyboardError) {
|
|
919
960
|
console.warn('⚠️ Keyboard capture failed to start:', keyboardError.message);
|
|
@@ -1004,7 +1045,8 @@ class MacRecorder extends EventEmitter {
|
|
|
1004
1045
|
|
|
1005
1046
|
// Native kayıt gerçekten başladığını kontrol etmek için polling başlat
|
|
1006
1047
|
let recordingStartedEmitted = false;
|
|
1007
|
-
|
|
1048
|
+
let checkRecordingStatus = null;
|
|
1049
|
+
const pollRecordingStatus = () => {
|
|
1008
1050
|
try {
|
|
1009
1051
|
const nativeStatus = nativeBinding.getRecordingStatus();
|
|
1010
1052
|
if (nativeStatus && !recordingStartedEmitted) {
|
|
@@ -1050,8 +1092,10 @@ class MacRecorder extends EventEmitter {
|
|
|
1050
1092
|
});
|
|
1051
1093
|
}
|
|
1052
1094
|
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
checkRecordingStatus = setInterval(pollRecordingStatus, 50);
|
|
1098
|
+
|
|
1055
1099
|
// Timeout fallback - 5 saniye sonra hala başlamamışsa emit et
|
|
1056
1100
|
setTimeout(() => {
|
|
1057
1101
|
if (!recordingStartedEmitted) {
|
|
@@ -1683,12 +1727,30 @@ class MacRecorder extends EventEmitter {
|
|
|
1683
1727
|
}
|
|
1684
1728
|
|
|
1685
1729
|
// Add sync metadata to first event only
|
|
1730
|
+
//
|
|
1731
|
+
// KRITIK: `videoStartTime` SADECE native'in bildirdigi gercek video
|
|
1732
|
+
// baslangici varsa yazilir. Onceden buraya `cursorCaptureSessionTimestamp`
|
|
1733
|
+
// yaziliyordu; o videonun ilk karesi DEGIL, dosya adi icin uretilen
|
|
1734
|
+
// oturum damgasi. Editor (cursorPlaybackTimeline) bu alani video t=0
|
|
1735
|
+
// sanip birebir telafi uyguladigi icin cursor kayiyordu.
|
|
1736
|
+
// Gercek deger yoksa alani hic yazmiyoruz -> editor heuristik
|
|
1737
|
+
// hizalamaya duser, ki yanlis bir offset uygulamaktan iyidir.
|
|
1686
1738
|
if (this.cursorCaptureFirstWrite && this.cursorCaptureSessionTimestamp) {
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1739
|
+
const knownVideoStart =
|
|
1740
|
+
Number(this.videoStartTimestamp) > 0
|
|
1741
|
+
? Number(this.videoStartTimestamp)
|
|
1742
|
+
: null;
|
|
1743
|
+
|
|
1744
|
+
cursorData._syncMetadata = knownVideoStart
|
|
1745
|
+
? {
|
|
1746
|
+
videoStartTime: knownVideoStart,
|
|
1747
|
+
cursorStartTime: this.cursorCaptureStartTime,
|
|
1748
|
+
offsetMs: this.cursorCaptureStartTime - knownVideoStart,
|
|
1749
|
+
}
|
|
1750
|
+
: {
|
|
1751
|
+
cursorStartTime: this.cursorCaptureStartTime,
|
|
1752
|
+
sessionTimestamp: this.cursorCaptureSessionTimestamp,
|
|
1753
|
+
};
|
|
1692
1754
|
}
|
|
1693
1755
|
|
|
1694
1756
|
// Sadece eventType değiştiğinde veya pozisyon değiştiğinde kaydet
|
|
@@ -1776,7 +1838,13 @@ class MacRecorder extends EventEmitter {
|
|
|
1776
1838
|
*/
|
|
1777
1839
|
_writeCursorSyncMetadata(cursorFilePath) {
|
|
1778
1840
|
const videoStartTime = Number(this.videoStartTimestamp);
|
|
1779
|
-
|
|
1841
|
+
// Cursor timeline'inin GERCEK referansi. Video baslangici okunabildiyse
|
|
1842
|
+
// timeline zaten ona hizalandi (delay 0); okunamadiysa syncTimestamp'e
|
|
1843
|
+
// dusuldu. Burada varsayim yapmak yerine kullanilan degeri yaziyoruz,
|
|
1844
|
+
// aksi halde editor var olmayan bir kaymayi telafi etmeye calisir.
|
|
1845
|
+
const cursorStartTime = Number(
|
|
1846
|
+
this.timelineStartTimestamp || this.syncTimestamp
|
|
1847
|
+
);
|
|
1780
1848
|
if (
|
|
1781
1849
|
!Number.isFinite(videoStartTime) ||
|
|
1782
1850
|
videoStartTime <= 0 ||
|
package/package.json
CHANGED
package/src/cursor_tracker.mm
CHANGED
|
@@ -153,23 +153,7 @@ static uint64_t FNV1AHash(const unsigned char *data, size_t length) {
|
|
|
153
153
|
return hash;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
static NSString*
|
|
157
|
-
if (!image) {
|
|
158
|
-
return nil;
|
|
159
|
-
}
|
|
160
|
-
NSRect imageRect = NSMakeRect(0, 0, [image size].width, [image size].height);
|
|
161
|
-
CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:nil hints:nil];
|
|
162
|
-
if (!cgImage) {
|
|
163
|
-
for (NSImageRep *rep in [image representations]) {
|
|
164
|
-
if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
|
|
165
|
-
cgImage = [(NSBitmapImageRep *)rep CGImage];
|
|
166
|
-
if (cgImage) {
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
156
|
+
static NSString* CursorImageFingerprintFromCGImage(CGImageRef cgImage, NSPoint hotspot) {
|
|
173
157
|
if (!cgImage) {
|
|
174
158
|
return nil;
|
|
175
159
|
}
|
|
@@ -223,6 +207,67 @@ static NSString* CursorImageFingerprintFromImage(NSImage *image, NSPoint hotspot
|
|
|
223
207
|
hash];
|
|
224
208
|
}
|
|
225
209
|
|
|
210
|
+
// NSImage'in VARSAYILAN temsilinden fingerprint. Hangi temsilin seçildigi ekranin
|
|
211
|
+
// backing scale'ine bagli (Retina 2x vs harici 1x), bu yuzden calisma aninda
|
|
212
|
+
// uretilen deger ekran konfigurasyonuna gore degisebilir.
|
|
213
|
+
static NSString* CursorImageFingerprintFromImage(NSImage *image, NSPoint hotspot) {
|
|
214
|
+
if (!image) {
|
|
215
|
+
return nil;
|
|
216
|
+
}
|
|
217
|
+
NSRect imageRect = NSMakeRect(0, 0, [image size].width, [image size].height);
|
|
218
|
+
CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:nil hints:nil];
|
|
219
|
+
if (!cgImage) {
|
|
220
|
+
for (NSImageRep *rep in [image representations]) {
|
|
221
|
+
if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
|
|
222
|
+
cgImage = [(NSBitmapImageRep *)rep CGImage];
|
|
223
|
+
if (cgImage) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return CursorImageFingerprintFromCGImage(cgImage, hotspot);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Bir cursor imajinin TUM temsilleri icin fingerprint uretir.
|
|
234
|
+
// KRITIK: MacBook kapagi kapaliyken tek harici monitorde calisirken (clamshell)
|
|
235
|
+
// veya ekranlar arasi gecerken NSImage farkli bir temsil donduruyor; tek fingerprint
|
|
236
|
+
// kaydedilirse eslesme kayboluyor ve cursor tipi yanlis tespit ediliyordu.
|
|
237
|
+
// Tum temsilleri kaydedince calisma aninda hangisi secilirse secilsin eslesme tutar.
|
|
238
|
+
static NSArray<NSString *>* CursorImageFingerprintsAllReps(NSImage *image, NSPoint hotspot) {
|
|
239
|
+
if (!image) {
|
|
240
|
+
return @[];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
NSMutableArray<NSString *> *fingerprints = [NSMutableArray array];
|
|
244
|
+
|
|
245
|
+
NSString *defaultFingerprint = CursorImageFingerprintFromImage(image, hotspot);
|
|
246
|
+
if (defaultFingerprint) {
|
|
247
|
+
[fingerprints addObject:defaultFingerprint];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (NSImageRep *rep in [image representations]) {
|
|
251
|
+
CGImageRef repImage = NULL;
|
|
252
|
+
if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
|
|
253
|
+
repImage = [(NSBitmapImageRep *)rep CGImage];
|
|
254
|
+
} else {
|
|
255
|
+
NSRect repRect = NSMakeRect(0, 0, [rep pixelsWide], [rep pixelsHigh]);
|
|
256
|
+
if (repRect.size.width <= 0 || repRect.size.height <= 0) {
|
|
257
|
+
repRect = NSMakeRect(0, 0, [rep size].width, [rep size].height);
|
|
258
|
+
}
|
|
259
|
+
repImage = [rep CGImageForProposedRect:&repRect context:nil hints:nil];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
NSString *repFingerprint = CursorImageFingerprintFromCGImage(repImage, hotspot);
|
|
263
|
+
if (repFingerprint && ![fingerprints containsObject:repFingerprint]) {
|
|
264
|
+
[fingerprints addObject:repFingerprint];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return fingerprints;
|
|
269
|
+
}
|
|
270
|
+
|
|
226
271
|
static NSString* CursorImageFingerprintUnsafe(NSCursor *cursor) {
|
|
227
272
|
if (!cursor) {
|
|
228
273
|
return nil;
|
|
@@ -313,11 +358,17 @@ static void AddStandardCursorFingerprint(NSCursor *cursor, NSString *cursorType)
|
|
|
313
358
|
if (!cursor || !cursorType) {
|
|
314
359
|
return;
|
|
315
360
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
361
|
+
// Tek bir temsil degil, tum temsiller kaydedilir -> ekran olcegi degisse bile
|
|
362
|
+
// (clamshell / harici monitor) eslesme korunur.
|
|
363
|
+
NSArray<NSString *> *fingerprints =
|
|
364
|
+
CursorImageFingerprintsAllReps([cursor image], [cursor hotSpot]);
|
|
365
|
+
for (NSString *fingerprint in fingerprints) {
|
|
366
|
+
// Ilk kayit kazanir: ayni fingerprint birden fazla cursor'a denk gelirse
|
|
367
|
+
// once eklenen (daha spesifik) tip korunur.
|
|
368
|
+
if (![g_cursorFingerprintMap objectForKey:fingerprint]) {
|
|
369
|
+
[g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
|
|
370
|
+
}
|
|
319
371
|
}
|
|
320
|
-
[g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
|
|
321
372
|
}
|
|
322
373
|
|
|
323
374
|
static void AddCursorIfAvailable(SEL selector, NSString *cursorType) {
|
package/src/mac_recorder.mm
CHANGED
|
@@ -1344,6 +1344,19 @@ Napi::Value GetVideoStartTimestamp(const Napi::CallbackInfo& info) {
|
|
|
1344
1344
|
}
|
|
1345
1345
|
|
|
1346
1346
|
// NAPI Function: Get Recording Status
|
|
1347
|
+
// Kayit baslatma gecikmesini azaltmak icin SCShareableContent'i onceden isitir.
|
|
1348
|
+
// Asenkron; hemen doner ve hicbir seyi bloklamaz.
|
|
1349
|
+
Napi::Value PrewarmScreenCapture(const Napi::CallbackInfo& info) {
|
|
1350
|
+
Napi::Env env = info.Env();
|
|
1351
|
+
|
|
1352
|
+
if (@available(macOS 12.3, *)) {
|
|
1353
|
+
[ScreenCaptureKitRecorder prewarmShareableContent];
|
|
1354
|
+
return Napi::Boolean::New(env, true);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
return Napi::Boolean::New(env, false);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1347
1360
|
Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
|
|
1348
1361
|
Napi::Env env = info.Env();
|
|
1349
1362
|
|
|
@@ -1711,6 +1724,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
1711
1724
|
exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
|
|
1712
1725
|
exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
|
|
1713
1726
|
exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
|
|
1727
|
+
exports.Set(Napi::String::New(env, "prewarmScreenCapture"), Napi::Function::New(env, PrewarmScreenCapture));
|
|
1714
1728
|
exports.Set(Napi::String::New(env, "getVideoStartTimestamp"), Napi::Function::New(env, GetVideoStartTimestamp));
|
|
1715
1729
|
exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
|
|
1716
1730
|
|
package/src/screen_capture_kit.h
CHANGED
|
@@ -7,6 +7,11 @@ API_AVAILABLE(macos(12.3))
|
|
|
7
7
|
|
|
8
8
|
+ (BOOL)isScreenCaptureKitAvailable;
|
|
9
9
|
|
|
10
|
+
// SCShareableContent'i onceden cekerek macOS ic cache'ini isitir.
|
|
11
|
+
// Kayit anindaki ilk cagri yavas oldugu icin (ozellikle ekran konfigurasyonu
|
|
12
|
+
// degistikten sonra) baslatma gecikmesini azaltir. Asenkron, hicbir seyi bloklamaz.
|
|
13
|
+
+ (void)prewarmShareableContent;
|
|
14
|
+
|
|
10
15
|
// MULTI-SESSION API: New session-based recording
|
|
11
16
|
+ (NSString *)startRecordingWithConfiguration:(NSDictionary *)config
|
|
12
17
|
delegate:(id)delegate
|
|
@@ -1039,6 +1039,25 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
1039
1039
|
return NO;
|
|
1040
1040
|
}
|
|
1041
1041
|
|
|
1042
|
+
+ (void)prewarmShareableContent {
|
|
1043
|
+
if (@available(macOS 15.0, *)) {
|
|
1044
|
+
// Kayit sirasindaki ilk SCShareableContent cagrisi yavas olabiliyor
|
|
1045
|
+
// (TCC kontrolu + pencere/ekran envanteri). Kayit baslamadan once bir kez
|
|
1046
|
+
// cagirip macOS'un ic cache'ini isitiyoruz. Sonuc kullanilmiyor.
|
|
1047
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
|
1048
|
+
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
|
|
1049
|
+
if (contentError) {
|
|
1050
|
+
MRLog(@"⚠️ Prewarm shareable content failed: %@", contentError.localizedDescription);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
MRLog(@"🔥 Prewarm: shareable content hazir (%lu ekran, %lu pencere)",
|
|
1054
|
+
(unsigned long)content.displays.count,
|
|
1055
|
+
(unsigned long)content.windows.count);
|
|
1056
|
+
}];
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1042
1061
|
+ (BOOL)startRecordingWithConfiguration:(NSDictionary *)config delegate:(id)delegate error:(NSError **)error {
|
|
1043
1062
|
if (!config) {
|
|
1044
1063
|
return NO;
|
package/src/window_selector.mm
CHANGED
|
@@ -157,6 +157,24 @@ static void ApplyStartRecordButtonIcon(NSButton *button) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// CGWindow (sol-üst orijin) <-> Cocoa (sol-alt orijin) dönüşümünde referans yükseklik
|
|
161
|
+
// DAİMA primary ekranın (screens[0]) yüksekliğidir. [NSScreen mainScreen] farenin/aktif
|
|
162
|
+
// pencerenin bulunduğu ekranı döner; çok ekranlı kurulumda kullanılırsa highlight kayar.
|
|
163
|
+
static CGFloat primaryScreenHeight() {
|
|
164
|
+
NSArray *screens = [NSScreen screens];
|
|
165
|
+
if ([screens count] == 0) return [[NSScreen mainScreen] frame].size.height;
|
|
166
|
+
return [[screens objectAtIndex:0] frame].size.height;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Bir NSScreen frame'ini (Cocoa global) CGWindow koordinat uzayına çevirir
|
|
170
|
+
static NSRect screenFrameInCGSpace(NSScreen *screen) {
|
|
171
|
+
NSRect f = [screen frame];
|
|
172
|
+
return NSMakeRect(f.origin.x,
|
|
173
|
+
primaryScreenHeight() - NSMaxY(f),
|
|
174
|
+
f.size.width,
|
|
175
|
+
f.size.height);
|
|
176
|
+
}
|
|
177
|
+
|
|
160
178
|
// Forward declarations
|
|
161
179
|
void cleanupWindowSelector();
|
|
162
180
|
void updateOverlay();
|
|
@@ -234,7 +252,8 @@ void updateScreenOverlays();
|
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
// Draw highlight rectangle for the selected window
|
|
237
|
-
|
|
255
|
+
// 1px stroke'un yarısı dışarı taşmasın diye 0.5px içeri al -> pencereye tam otursun
|
|
256
|
+
NSBezierPath *highlightPath = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.highlightFrame, 0.5, 0.5)
|
|
238
257
|
xRadius:8.0
|
|
239
258
|
yRadius:8.0];
|
|
240
259
|
|
|
@@ -852,9 +871,10 @@ void updateOverlay() {
|
|
|
852
871
|
// Get current cursor position
|
|
853
872
|
NSPoint mouseLocation = [NSEvent mouseLocation];
|
|
854
873
|
// Convert from NSEvent coordinates (bottom-left) to CGWindow coordinates (top-left)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
CGPoint globalPoint = CGPointMake(mouseLocation.x,
|
|
874
|
+
// NOT: referans DAİMA primary ekran yüksekliği; mainScreen kullanılırsa imleç
|
|
875
|
+
// farklı bir ekrana geçtiğinde koordinat kayar ve yanlış pencere seçilir.
|
|
876
|
+
CGPoint globalPoint = CGPointMake(mouseLocation.x,
|
|
877
|
+
primaryScreenHeight() - mouseLocation.y);
|
|
858
878
|
|
|
859
879
|
// Find window under cursor (no need to refresh g_allWindows frequently since windows can't move)
|
|
860
880
|
NSDictionary *windowUnderCursor = getWindowUnderCursor(globalPoint);
|
|
@@ -961,27 +981,21 @@ void updateOverlay() {
|
|
|
961
981
|
CGFloat windowCenterY = y + height / 2;
|
|
962
982
|
|
|
963
983
|
for (NSScreen *screen in screens) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
CGFloat screenRight = screenFrame.origin.x + screenFrame.size.width;
|
|
970
|
-
|
|
971
|
-
if (windowCenterX >= screenLeft && windowCenterX <= screenRight &&
|
|
972
|
-
windowCenterY >= screenBottom && windowCenterY <= screenTop) {
|
|
984
|
+
// Pencere koordinatları CGWindow uzayında; ekran frame'ini de aynı uzaya çevir
|
|
985
|
+
NSRect cgScreen = screenFrameInCGSpace(screen);
|
|
986
|
+
|
|
987
|
+
if (windowCenterX >= NSMinX(cgScreen) && windowCenterX <= NSMaxX(cgScreen) &&
|
|
988
|
+
windowCenterY >= NSMinY(cgScreen) && windowCenterY <= NSMaxY(cgScreen)) {
|
|
973
989
|
windowScreen = screen;
|
|
974
990
|
break;
|
|
975
991
|
}
|
|
976
992
|
}
|
|
977
|
-
|
|
993
|
+
|
|
978
994
|
// Use main screen if no specific screen found
|
|
979
995
|
if (!windowScreen) windowScreen = [NSScreen mainScreen];
|
|
980
|
-
|
|
981
|
-
// Convert coordinates from CGWindow (top-left) to
|
|
982
|
-
|
|
983
|
-
CGFloat screenHeight = screenFrame.size.height;
|
|
984
|
-
CGFloat adjustedY = screenHeight - y - height;
|
|
996
|
+
|
|
997
|
+
// Convert coordinates from CGWindow (top-left) to Cocoa global (bottom-left)
|
|
998
|
+
CGFloat adjustedY = primaryScreenHeight() - y - height;
|
|
985
999
|
|
|
986
1000
|
// Window coordinates are in global space, overlay frame should be screen-relative
|
|
987
1001
|
// Keep X coordinate as-is (already in global space which is what we want)
|
|
@@ -1014,19 +1028,19 @@ void updateOverlay() {
|
|
|
1014
1028
|
NSInteger targetScreenIndex = -1;
|
|
1015
1029
|
NSScreen *targetScreen = nil;
|
|
1016
1030
|
|
|
1017
|
-
// Find screen containing this window
|
|
1031
|
+
// Find screen containing this window (karşılaştırma CGWindow uzayında yapılmalı)
|
|
1018
1032
|
for (NSInteger i = 0; i < [allScreens count]; i++) {
|
|
1019
1033
|
NSScreen *screen = [allScreens objectAtIndex:i];
|
|
1020
|
-
NSRect
|
|
1021
|
-
|
|
1034
|
+
NSRect cgScreen = screenFrameInCGSpace(screen);
|
|
1035
|
+
|
|
1022
1036
|
// Check if window center is within this screen bounds
|
|
1023
1037
|
CGFloat windowCenterX = x + width/2;
|
|
1024
1038
|
CGFloat windowCenterY = y + height/2;
|
|
1025
|
-
|
|
1026
|
-
if (windowCenterX >=
|
|
1027
|
-
windowCenterX <=
|
|
1028
|
-
windowCenterY >=
|
|
1029
|
-
windowCenterY <=
|
|
1039
|
+
|
|
1040
|
+
if (windowCenterX >= NSMinX(cgScreen) &&
|
|
1041
|
+
windowCenterX <= NSMaxX(cgScreen) &&
|
|
1042
|
+
windowCenterY >= NSMinY(cgScreen) &&
|
|
1043
|
+
windowCenterY <= NSMaxY(cgScreen)) {
|
|
1030
1044
|
targetScreenIndex = i;
|
|
1031
1045
|
targetScreen = screen;
|
|
1032
1046
|
break;
|
|
@@ -1043,9 +1057,10 @@ void updateOverlay() {
|
|
|
1043
1057
|
WindowSelectorOverlayView *targetOverlayView = [g_perScreenOverlayViews objectAtIndex:targetScreenIndex];
|
|
1044
1058
|
NSRect targetScreenFrame = [targetScreen frame];
|
|
1045
1059
|
|
|
1046
|
-
// Calculate LOCAL coordinates within target screen
|
|
1060
|
+
// Calculate LOCAL coordinates within target screen
|
|
1061
|
+
// CG (sol-üst, primary orijinli) -> Cocoa global (sol-alt) -> ekran-yerel
|
|
1047
1062
|
CGFloat localX = x - targetScreenFrame.origin.x;
|
|
1048
|
-
CGFloat localY = (
|
|
1063
|
+
CGFloat localY = (primaryScreenHeight() - y - height) - targetScreenFrame.origin.y;
|
|
1049
1064
|
|
|
1050
1065
|
NSLog(@"🎯 Window on Screen %ld: Global(%.0f,%.0f) → Local(%.0f,%.0f)",
|
|
1051
1066
|
targetScreenIndex, (CGFloat)x, (CGFloat)y, localX, localY);
|
|
@@ -2177,12 +2192,16 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
|
2177
2192
|
NSRect screenFrame = [screen frame];
|
|
2178
2193
|
|
|
2179
2194
|
// Create per-screen overlay window
|
|
2195
|
+
// NOT: screen: parametresi non-nil verilirse contentRect o ekranın sol-alt köşesine
|
|
2196
|
+
// GÖRE yorumlanır; global screenFrame ile birlikte çift offset oluşur. nil verip
|
|
2197
|
+
// frame'i global koordinatta açıkça set ediyoruz.
|
|
2180
2198
|
NSWindow *screenOverlay = [[NoFocusWindow alloc] initWithContentRect:screenFrame
|
|
2181
2199
|
styleMask:NSWindowStyleMaskBorderless
|
|
2182
2200
|
backing:NSBackingStoreBuffered
|
|
2183
2201
|
defer:NO
|
|
2184
|
-
screen:
|
|
2185
|
-
|
|
2202
|
+
screen:nil];
|
|
2203
|
+
[screenOverlay setFrame:screenFrame display:NO];
|
|
2204
|
+
|
|
2186
2205
|
// Create per-screen overlay view
|
|
2187
2206
|
WindowSelectorOverlayView *overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:NSMakeRect(0, 0, screenFrame.size.width, screenFrame.size.height)];
|
|
2188
2207
|
[screenOverlay setContentView:overlayView];
|