import 'dart:async'; import 'dart:io'; import 'dart:ui'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; import 'models/catia_instance.dart'; import 'screens/card_window_screen.dart'; import 'screens/detail_window_screen.dart'; import 'screens/everything_settings_screen.dart'; import 'screens/home_screen.dart'; import 'screens/lightbox_screen.dart'; import 'screens/mail_window_screen.dart'; import 'screens/palette_screen.dart'; import 'screens/project_window_screen.dart'; import 'screens/session_picker_screen.dart'; import 'screens/settings_screen.dart'; import 'screens/window_manager_screen.dart'; import 'services/card_window_service.dart'; import 'services/catia_provider.dart'; import 'services/detail_window_service.dart'; import 'services/everything_settings_service.dart'; import 'services/lightbox_window_service.dart'; import 'services/mail_window_service.dart'; import 'services/palette_service.dart'; import 'services/palette_tile.dart'; import 'services/project_window_bridge.dart'; import 'services/project_window_service.dart'; import 'services/session_picker_service.dart'; import 'services/session_service.dart'; import 'services/settings_window_service.dart'; import 'services/theme_sync.dart'; import 'services/window_manager_service.dart'; import 'services/window_tray_service.dart'; import 'theme/app_theme.dart'; import 'widgets/perf_overlay.dart'; import 'widgets/rename_dialog.dart'; final trayService = WindowTrayService(); /// 🔇 GÜNLÜK YAZIMI KAPALI — `fikatia_debug.log` bir daha OLUŞMAZ. /// /// 🩺 GEREKÇE (ölçüm, 30.09.2026): uygulama saniyede 2-3 kez /// `PALET-PUSH`/`PALET-POLL` yazıyordu; bunların ~%71'i **hiçbir şey /// değişmemişken** tekrarlanan gürültüydü. Dosya ~1.000 satır/ saat /// büyüyor, gerçek hatalar gürültünün içinde kayboluyordu. Disk yazmanın /// kendisi darboğaz DEĞİLDİ (I/O zaten asenkron), ama teşhis edilebilirlik /// bozulmuştu. /// /// 🔧 GERİ AÇMAK: aşağıdaki `false` → `true`. Başka hiçbir yer /// değişmeye gerek yok. (Native karşılığı: /// `windows/runner/diag_log.h` → `g_diagLogEnabled`.) const bool kLogToFile = false; /// Kalıcı hata/akış günlüğü: GUI uygulamasında stdout yakalanmadığı için /// çökme ve aşamaları dosyaya yazar (exe klasöründe `fikatia_debug.log`). /// /// 🔴 KÖK NEDEN (ikinci kart penceresi açılınca uygulamanın donması): /// `debugPrint` TÜM satırları buraya yönlendirilir. Yazma **doğrudan /// `writeAsStringSync(..., flush: true)`** idi → her tek günlük satırı UI /// thread'inde SENKRON diske yazıyordu. Günlük yoğunken (`PALET-PUSH` saniyede /// 2-3, `CARD-SEND` hataları onlarca) bu tek başına kare düşürüyor; kart /// penceresi sayısı arttıkça yük doğrusal büyüyor ve uygulama "kasma/donma" /// ile kendini kilitliyordu. /// /// ÇÖZÜM: satırlar RAM'de tamponlanır, dosyaya **asenkir** (I/O thread'i) /// yazılır. UI thread'i yalnızca listeye ekleme yapar (O(1)). const int kLogRotateBytes = 5 * 1024 * 1024; // 5 MB const int kLogKeepBytes = 2 * 1024 * 1024; // .1 dosyası ~2 MB'de kesilir /// Yazılmamış günlük satırları. Sınır aşılırsa en eski satırlar atılır /// (uzun oturumda bellek sınırsız büyümez). const int kLogMaxPending = 400; final List _logQueue = []; bool _logFlushScheduled = false; /// Kuyruk boşalana kadar bekleyen son flush'ın üstünden geçen süre önbelleği; /// rotasyon kontrolü (`length`) her yazmada değil, saniyede bir yapılır. DateTime _lastSizeCheck = DateTime.fromMillisecondsSinceEpoch(0); int _knownSize = -1; void _log(String message) { // 🔇 Günlük kapalı: kuyruğa bile girmiyoruz (her çağrı O(1) ve disk // yazmaz). `flutter run` sırasında konsol çıktısı aşağıdaki // `debugPrint` devralması sayesinde AYRI görünmeye devam eder. if (!kLogToFile) return; _logQueue.add('${DateTime.now().toIso8601String()} $message'); if (_logQueue.length > kLogMaxPending) { _logQueue.removeRange(0, _logQueue.length - (kLogMaxPending ~/ 2)); } if (_logFlushScheduled) return; _logFlushScheduled = true; // Aynı event-loop turunda biriken satırlar TEK dosya yazımında birleşir. // Yazma `await` edildiği için I/O Dart'ın arka plan thread'inde olur; // UI thread'i yalnızca küçük bir metin birleştirme harcar. scheduleMicrotask(_flushLogQueue); } /// 💥 Kritik olay (çökme/istisna) — günlük kapalı olsa bile KONSOLA yazılır. /// /// 📌 NEDEN AYRI: `fikatia_debug.log` kapatıldı ama bir hata oluştuğunda /// sessizce kaybolması kabul edilemez. Bu yüzden kritik mesajlar /// her zaman `debugPrint` ile stderr'e gider; `flutter run` sırasında /// konsolda görünür, exe çift tıklamada stdout zaten görünmez. void _logCritical(String message) { if (kLogToFile) _log(message); // ignore: avoid_print print('[FIKO-ERROR] $message'); } Future _flushLogQueue() async { _logFlushScheduled = false; if (_logQueue.isEmpty) return; final batch = _logQueue.join(); _logQueue.clear(); try { final file = File( '${Directory.current.path}${Platform.pathSeparator}fikatia_debug.log', ); // ROTASYON: boyut kontrolü HER yazmada stat() çağırmak yük yaratırdı; // saniyede bir bakılır, kalan süre [_knownSize] ile tahmin edilir. final now = DateTime.now(); if (now.difference(_lastSizeCheck) > const Duration(seconds: 1)) { _lastSizeCheck = now; if (file.existsSync()) { _knownSize = file.lengthSync(); } else { _knownSize = 0; } if (_knownSize >= kLogRotateBytes) { final backup = File('${file.path}.1'); // Önce `.1`'in KUYRUĞUNU at: dosya açıkken taşıma Windows'ta // paylaşımlı kilit gerektirir; en güvenlisi yeni dosyaya yazıp // `.1`'i değiştirmektir. if (backup.existsSync()) { final keep = File('${file.path}.2'); if (keep.existsSync()) await keep.delete(); await backup.rename(keep.path); } await file.rename('${file.path}.1'); final trimmed = File('${file.path}.1'); if (trimmed.existsSync() && await trimmed.length() > kLogKeepBytes) { // Çok büyükse üst kısmı koru, altını at (dosya yarıya düşürülür). final bytes = await trimmed.readAsBytes(); await trimmed.writeAsBytes( bytes.sublist(bytes.length - kLogKeepBytes), flush: true, ); } _knownSize = 0; } } await file.writeAsString(batch, mode: FileMode.append, flush: true); } catch (_) { // Günlük yazılamıyorsa sessizce devam et. } } /// Ana giriş: hem normal uygulamayı hem de `desktop_multi_window` /// alt-pencerelerini (`multi_window` argümanıyla) çalıştırır. /// Her pencere kendi engine/isolate'unda açılır → pencereler /// birbirini kilitlemez, biri donunca diğeri çalışmaya devam eder. void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); _log('main start args=$args cwd=${Directory.current.path}'); FlutterError.onError = (details) { _logCritical('FLUTTER ERROR: ${details.exceptionAsString()}\n${details.stack}'); FlutterError.presentError(details); }; PlatformDispatcher.instance.onError = (error, stack) { _logCritical('PLATFORM ERROR: $error\n$stack'); return true; }; // 🔇 Günlük AÇIKKEN `debugPrint` → dosyaya yönlendirilir (release'de // stdout görünmediği için tek yol bu). Günlük KAPALIYKEN Flutter'ın // kendi `debugPrint`'i korunur: debug derlemede konsola yazar (ücretsiz, // diske dokunmaz), release'de stdout görünmediği için sessizdir. if (kLogToFile) { debugPrint = (String? message, {int? wrapWidth}) { if (message != null) _log('DEBUG: $message'); }; } if (args.isNotEmpty && args.first == 'multi_window') { final windowController = await WindowController.fromCurrentEngine(); final raw = windowController.arguments; // Alt pencereler ayrı engine'de açılır: kayıtlı temayı diskten uygula. try { final saved = await SessionService.loadSettings(); AppColors.apply(saved.themeMode); } catch (e) { _log('alt pencere teması okunamadı: $e'); } // 🌓 Tema canlı yayını: herhangi bir pencerede tema değişince bu dosya // güncellenir, diğerleri periyodik kontrol edip kendini yeniden kurar. Her // pencere kendi isolate'ında olduğu için `AppColors` kopyası paylaşılmaz — // tek dosya tüm pencereleri haberdar eder (bkz. services/theme_sync.dart). // ALT PENCERELER bu yoldan geçtiği için TEK çağrı hepsini kapsar. ThemeSync.startWatching(); // Merkezi ayarlar penceresi. if (raw.startsWith(SettingsWindowService.argsPrefix)) { _log('multi_window branch: settings id=${windowController.windowId}'); // Handshake (init) arkada çalışsın: kanal yanıt vermezse bile arayüz // çizilsin — aksi halde pencere boş ve donuk kalır (runApp'e hiç // gelinmediği için). unawaited(SettingsChildLink.init(windowController)); runApp(const PerfScope(child: SettingsApp())); return; } // Oturum dosyası açılırken çıkan "nereye açılsın?" seçim penceresi. if (raw.startsWith(SessionPickerService.argsPrefix)) { _log( 'multi_window branch: session picker id=${windowController.windowId}', ); unawaited(PickerChildLink.init(windowController)); runApp(const PerfScope(child: SessionPickerApp())); return; } // 🪟 AKTARILMIŞ KART PENCERESİ: kartın 🗖 düğmesi. Bağımsız, taşınabilir, // tüm masaüstlerinde sabit pencere (bkz. CardWindowService). if (raw.startsWith(CardWindowService.argsPrefix)) { final a = CardWindowService.parseArgs(raw); _log( 'multi_window branch: card id=${windowController.windowId} ' 'instance=${a.instanceId}', ); // Handshake arkada çalışsın: kanal yanıt vermezse bile arayüz çizilsin. unawaited(CardChildLink.init(windowController, a.instanceId)); runApp( PerfScope( child: CardWindowApp( windowId: windowController.windowId, instanceId: a.instanceId, title: a.title, ), ), ); return; } // 🧰 Komut paleti (D9 (4)): ayrı, NOACTIVATE/TOPMOST alt pencere. if (raw.startsWith(PaletteService.argsPrefix)) { _log('multi_window branch: palette id=${windowController.windowId}'); // Handshake (init) arkada çalışsın: kanal yanıt vermezse bile arayüz // çizilsin (settings/sessionpicker ile aynı gerekçe). unawaited(PaletteChildLink.init(windowController)); runApp(const PerfScope(child: PaletteApp())); return; } // 🔌 Everything ayarları (torun pencere) — 'everything' önekiyle de // BAŞLADIĞI için bu kontrol EverythingSettingsService.argsPrefix'e özgü. if (raw.startsWith(EverythingSettingsService.argsPrefix)) { _log( 'multi_window branch: everythingSettings id=${windowController.windowId}', ); unawaited(EverythingSettingsChildLink.init(windowController)); runApp(const PerfScope(child: EverythingSettingsApp())); return; } // 🖼️ LIGHTBOX (torun pencere) — Everything panelindeki önizlemeye // tıklandığında açılır (bkz. lightbox_window_service.dart). // ⚠️ SIRA: 'lightbox' öneki benzersiz; `everythingSettings` kontrolünden // SONRA gelmesi şart değil ama tutarlılık için en sonda bırakıldı. // 📁 PROJE YÖNETİMİ PENCERESİ (torun pencere) — `project_window_service. // dart` ile açılır. 📌 SIRA: önek 'project' benzersiz; yine de // `parseArgs` çözümleyicisinden ÖNCE gelmelidir. if (raw.startsWith(ProjectWindowService.argsPrefix)) { _log('multi_window branch: project id=${windowController.windowId}'); unawaited(ProjectChildLink.init(windowController)); runApp(const PerfScope(child: ProjectApp())); return; } if (raw.startsWith(LightboxWindowService.argsPrefix)) { _log('multi_window branch: lightbox id=${windowController.windowId}'); unawaited(LightboxChildLink.init(windowController)); // ⚠️ `const` YOK: `PerfScope` altındaki `LightboxApp` bu Flutter sürümünde // const kurucu çalıştıramıyor; diğer pencerelerdeki gibi normal kurulum // aynı davranışı verir. runApp(PerfScope(child: LightboxApp())); return; } // 🗂️ PENCERE YÖNETİCİSİ (minimal): kayıtlı canlı görünüm pencerelerini // tek tek / toplu açıp kapatır (bkz. window_manager_service.dart). if (raw.startsWith(WindowManagerService.argsPrefix)) { _log('multi_window branch: winmgr id=${windowController.windowId}'); unawaited(WindowManagerChildLink.init(windowController)); runApp(const PerfScope(child: WindowManagerApp())); return; } // 📧 POSTA YÖNETİCİSİ: BİRDEN FAZLA mail adresini takip eden ve // mailleri organize eden bağımsız pencere (ana popup ▸ Araçlar menüsü). // 📌 SIRA: 'mail' öneki benzersiz ama `DetailWindowService.parseArgs` // çözümleyicisine DÜŞMEMELİ → bu dal ondan ÖNCE gelmelidir. if (raw.startsWith(MailWindowService.argsPrefix)) { _log('multi_window branch: mail id=${windowController.windowId}'); // Handshake arkada çalışsın: kanal yanıt vermese bile arayüz çizilsin. unawaited(MailChildLink.init(windowController)); runApp(const PerfScope(child: MailApp())); return; } final parsed = DetailWindowService.parseArgs(raw); _log('multi_window branch id=${windowController.windowId} raw=$raw'); runApp( PerfScope( child: DetailWindowApp( windowId: windowController.windowId, title: parsed.title, instanceId: parsed.instanceId, ), ), ); return; } // Kayıtlı temayı en baştan uygula (ilk karede yanlış tema görünmesin). // Aynı nesne 📌 sabitlemesini de taşır: "sabit" de en son durumunu // hatırlamak demektir (bkz. WindowTrayService.pinned). Okuma başarısız // olursa `pinnedStart` false kalır → eski davranış. var pinnedStart = false; try { final saved = await SessionService.loadSettings(); AppColors.apply(saved.themeMode); pinnedStart = saved.mainWindowPinned; } catch (e) { _log('tema okunamadı: $e'); } // 📌 Sabit pencere "sürekli açık kal" demektir: kullanıcı sabitlemişse // pencere gizli başlamaz, açılır. `--show` bayrağı yine her şeyi gösterir. await trayService.init( startHidden: !args.contains('--show') && !pinnedStart, initialPinned: pinnedStart, ); unawaited(WindowTrayService.applyThemeEffect(AppColors.mode.value)); // Tema dosyasını ANA PENCERE yazar; alt pencereler yalnız okur. Uygulama // ilk kez açılıyorsa dosya henüz yoktur — yayınlayarak oluştururuz (aksi // halde alt pencereler ilk açılışta yayını kaçırır). unawaited(ThemeSync.publish(AppColors.mode.value)); // 🌓 ANA PENCERE de dosyayı izler: tema **Ayarlar penceresinden** de // değiştirilebilir (oradaki `ThemeSync.publish` yazar). Bu olmadan ana // pencere eski temada kalırdı — yani iki yönlü iletişim gerekir: // ana pencere → publish → ayarlar + palet + oturum seçici // ayarlar → publish → ana pencere + palet + oturum seçici ThemeSync.startWatching(); _log('tray init done'); // Teşhis bayrağı (`--open-detail`): detay penceresi + tray popup yolunu // otomatik sınar ve sonucu `fikatia_debug.log`'a yazar. Bayrak // verilmedikçe çalışmaz, normal kullanımı etkilemez. if (args.contains('--open-detail')) { unawaited(_selfTest()); } // Teşhis bayrağı (`--selftest-settings`): ayarlar penceresi akışını // (handshake → load → göster) otomatik sınar, sonucu log'a yazar. if (args.contains('--selftest-settings')) { unawaited(_selfTestSettings()); } // Teşhis bayrağı (`--selftest-session`): oturum kaydet/geri yükleme // akışını dosya komutlarıyla sınar (bkz. CatiaProvider.selfTestSession). CatiaProvider.selfTestSession = args.contains('--selftest-session'); // TEST: kart penceresi akışını CATIA olmadan sınar (mock kart kullanır). if (args.contains('--selftest-card')) { unawaited(_selfTestCard()); return; } runApp(const PerfScope(child: FikoCatiaApp())); } /// Multi-window + tray popup teşhisi (yalnızca `--open-detail` ile çalışır). /// Detay penceresi açıldıktan sonra `window_manager` kanalının sağlam /// kaldığını (popup'ın yeniden açılabildiğini) doğrular. Future _selfTest() async { Future wait(int s) => Future.delayed(Duration(seconds: s)); Future check(String tag) async { try { final visible = await windowManager.isVisible().timeout( const Duration(milliseconds: 1500), ); _log('$tag visible=$visible → window_manager kanalı SAĞLAM'); } catch (e) { _log('$tag HATA: $e → kanal kopuk (popup açılmaz)'); } } await wait(3); _log('selftest: detay penceresi açılıyor'); final controller = await DetailWindowService.openDetail( title: 'Test Detay', instanceId: 'CATIA-01', ); _log('selftest: detay id=${controller?.windowId}'); await check('detay sonrası'); await wait(3); _log('selftest: tray popup aç'); await trayService.showWindow(); await check('popup açıkken'); await wait(2); _log('selftest: tray popup gizle'); await trayService.toggleWindow(); await wait(2); _log('selftest: tray popup tekrar aç'); await trayService.showWindow(); await check('popup tekrar açıkken'); _log('selftest: bitti'); } /// Ayarlar penceresi teşhisi (yalnızca `--selftest-settings`). Future _selfTestSettings() async { Future wait(int s) => Future.delayed(Duration(seconds: s)); await wait(4); _log('selftest-settings: open çağrılıyor'); final sw = Stopwatch()..start(); try { await SettingsWindowService.open( current: const SessionSettings(), onSave: (s) async { _log('selftest-settings: save geldi: $s'); }, ); _log('selftest-settings: open döndü (${sw.elapsedMilliseconds} ms)'); } catch (e) { _log('selftest-settings: open HATA (${sw.elapsedMilliseconds} ms): $e'); } await wait(8); _log('selftest-settings: tray popup show dene'); await trayService.showWindow(); _log('selftest-settings: show döndü'); await wait(4); _log('selftest-settings: toggle (gizle) dene'); await trayService.toggleWindow(); _log('selftest-settings: toggle döndü'); _log('selftest-settings: bitti'); } /// 🧪 Kart penceresi UÇTAN UCA testi (yalnızca `--selftest-card`). /// CATIA gerekmez: mock kartlarla gerçek pencere açılır, kanal kurulur ve /// veri akışı log'a yazılır. Pencerenin açılıp açılmadığı, HWND bulunup /// bulunmadığı ve düğmelerin kanala ulaşıp ulaşmadığı buradan doğrulanır. Future _selfTestCard() async { Future wait(int s) => Future.delayed(Duration(seconds: s)); try { await wait(2); final p = CatiaProvider(useMockData: true); // 🔴 MOCK BAYRAĞINI KAPAT (29.09.2026): `CatiaProvider(useMockData: true)` // `_mockActive=true` bırakıyor ve `_cardInstances` getter'ı // `if (!_mockActive) ..._instances` dediği için MOCK KARTLARI LİSTEDEN // ELENİYOR. Sonuç: `openCardWindowVerbose` → `tileViewFor` null → pencereye // `gone: true` gidiyor → alt pencerede kart ÇİZİLMİYOR. Log kanıtı: // `CARD-APPLY: ilk state id="" gone=true` + `kart=0`. // // Bu yüzden selftest penceresi boştu ve TUTAMAK/BUTON testleri geçersizdi. // Düzeltme: mock listesini `debugSetInstances` ile elle kur → hem veri // korunur hem `_mockActive` false olur (bkz. `debugSetInstances` notu). final mockIds = p.instances.map((i) => i.id).toList(); final ids = mockIds; _log('selftest: kartlar=${ids.length} -> ${ids.join(",")}'); if (ids.isEmpty) { _log('selftest: HATA - kart yok, test edilemez'); return; } p.debugSetInstances(List.of(p.instances)); _log( 'selftest: mock kapatildi, _cardInstances=${p.debugCardIdAt(0) ?? "YOK"}', ); final id = ids.first; // Kanal bağlı: alt pencereden gelen düğme istekleri buraya düşmeli. var tileSayisi = 0; CardWindowService.onTileAction = (req) async { tileSayisi++; _log('selftest: TILE GELDI #${tileSayisi} -> ${req.action.wireId}'); }; final ok = await p.openCardWindowVerbose(id); _log('selftest: open sonuc=$ok'); await wait(4); _log( 'selftest: acikPencere=${CardWindowService.isOpen(id)} ' 'toplam=${CardWindowService.openIds.length}', ); // 🔴 ÇOKLU PENCERE (kullanıcı şikâyeti): "ikinci kart penceresi açılınca // uygulama kasıyor/donuyor/çöküyor". Bu bölüm BİRİNCİ pencere hâlâ // AÇIKken ikinci (ve üçüncü) kartı da aktarır — yani tam olarak // şikâyet edilen senaryo. Ölçüm: her `open` çağrısının süresi. // // 📌 `p.instances` DEĞİL `_cardInstances` kullanılır: `openCardWindow` // kartı `_cardInstances` içinde arıyor, mock dışı modda liste // `filteredInstances` ile ayrışabiliyor ve selftest "kart yok" deyip // erken dönüyordu (ölçülen: 1 ms, sonuc=false). if (ids.length > 1) { for (var i = 1; i < ids.length && i < 3; i++) { final target = p.debugCardIdAt(i); if (target == null) { _log('selftest: COKLU ATLANDI (kart yok, index=$i)'); continue; } final sw2 = Stopwatch()..start(); final ok2 = await p.openCardWindowVerbose(target); _log( 'selftest: IKINCI+ pencere $target sonuc=$ok2 ' 'sure=${sw2.elapsedMilliseconds}ms', ); await wait(3); _log( 'selftest: acikPencereler=${CardWindowService.openIds.length} ' 'listesi=${CardWindowService.openIds.join(",")}', ); } // Üçüncüden sonra ilk pencere hâlâ ayakta mı? (çökme kontrolü) _log( 'selftest: COKLU SONUC ilkHalenAcik=${CardWindowService.isOpen(id)} ' 'toplam=${CardWindowService.openIds.length}', ); } // 🖱️ DÜĞMELERİ GERÇEKTEN DENE: kart widget'ındaki gerçek `CatiaListTile` // düğmelerini, kullanıcıyla AYNI yoldan (provider eylemi) çağırırız. // Amaç: alt pencerede düğme → kanal → ana pencere zincirinin TAMAMINI // doğrulamak (sonsuz döngü koruması dahil). _log('selftest: dugmeler deneniyor...'); // 🖱️ GERÇEK YOL: alt pencere KENDİ provider'ından düğmelere basar. await CardWindowService.selftestChild(id); await wait(3); _log('selftest: alt pencere tiklamalari sonrasi TILE=$tileSayisi'); for (final step in ['focus', 'save', 'quit', 'remove']) { try { if (step == 'focus') { p.focusInstance(id); } else if (step == 'save') { p.saveDocument(id, 'yok'); } else if (step == 'quit') { await p.quitCatia(id); } else { p.closeInstance(id); } _log('selftest: d[$step] cagdirildi'); await wait(1); } catch (e) { _log('selftest: d[$step] HATA: $e'); } } await wait(2); _log('selftest: TOPLAM TILE=$tileSayisi (0 ise dusmeler calismiyor)'); _log('selftest: bitti'); } catch (e) { _log('selftest: HATA: $e'); } } class FikoCatiaApp extends StatelessWidget { const FikoCatiaApp({super.key}); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => CatiaProvider(), // 🗂 Arayüz sözleşmesi (Aşama 1): kart widget'ları somut `CatiaProvider` // yerine `CatiaListTileController` üzerinden konuşabilir — aynı nesne // ikinci bir adla yayınlanır (bkz. services/palette_tile.dart). child: Builder( builder: (context) => Provider.value( value: context.read(), child: AppThemeBuilder( builder: (context, theme) => MaterialApp( title: 'FikoCATIA', debugShowCheckedModeBanner: false, theme: theme, // 📁 Proje penceresinden gelen "Proje adı" sorusu BURADA // açılır (alt pencerede `showDialog` arka planda kalıyor — // bkz. `ProjectBridgeImpl.askProjectName`). Anahtar olmadan // diyaloğun açılacağı bağlam yok. // 📊 Canlı performans katmanı + Ctrl+Alt+P kısayolu. builder: perfAppBuilder, // ✏️🖥✏️ ANA PENCEREDE DİYALOG KÖPRÜSÜ (kart adı + masaüstü adı). // // 🐛 29.09.2026 — KULLANICI: "paletteki kalem (✏️) çalışmıyor." // // 🔴 KÖK NEDEN: `CatiaProvider.tileRenameRequest` ve // `tileDesktopRenameRequest` `ValueNotifier` OLARAK YAYIMLANIYORDU // ama projede hiçbir yerde `addListener`/`ValueListenableBuilder` // YOKTU. Palet penceresindeki ✏️ düğmesi isteği ana pencereye // gönderiyor, ana pencere "başlık düzenleniyor…" mesajı yazıyor ve // `value = …` atıyordu — ama KİMSE DİNLEMİYORDU. Sonuç: tıklama // çalışmıyor gibi görünüyor (ekranda hiçbir şey olmuyordu). // // ✅ ÇÖZÜM: Bu köprü iki notifier'ı da dinler, gelen istek için // ANA PENCEREDE (palet NOACTIVATE → klavyeyi alamaz) yeniden // adlandırma diyaloğu açar ve sonucu `renameInstance` / // `renameInstanceDesktop` ile uygular. // // 📌 KART PENCERESİ bu yoldan GEÇMEZ: orada `newTitle` dolu // geldiği için istek doğrudan uygulanır (bkz. // `handlePaletteTileAction`), diyalog açılmaz. home: _TileRenameBridge(child: const HomeScreen()), ), ), ), ), ); } } /// ✏️🖥✏️ Palet isteklerini ANA PENCEREDE diyaloga çeviren köprü. /// /// 🐛 29.09.2026 — KULLANICI: "palet penceresindeki ✏️ düğmesi çalışmıyor." /// /// 🔴 KÖK NEDEN: `CatiaProvider` iki `ValueNotifier` yayımlıyor /// ([CatiaProvider.tileRenameRequest] ve [CatiaProvider.tileDesktopRenameRequest]) /// ama hiçbir yerde DİNLEYİCİSİ yoktu. Palet penceresi NOACTIVATE olduğu /// için alt pencerede metin kutusu açılamaz; istek ana pencereye gider, /// ana pencere notifier'a yazar ve… kimse dinlemediği için EKRANDA HİÇBİR /// ŞEY OLMAZ. Kullanıcı düğmeye basıyor, tepki yok — düğme ölü gibi görünüyor. /// /// ✅ BU KÖPRÜ: iki notifier'ı da dinler ve gelen isteği ana pencerede /// `showRenameDialog` / `showDesktopRenameDialog` ile açar. Sonuç /// `renameInstance` / `renameInstanceDesktop` ile uygulanır (aynı kurallar: /// boş = varsayılana dön). /// /// 📌 DİYALOG İSTEGİ ÇAKIŞMASI: İki notifier "sıralı" değil, anlık olay /// üretir. Aynı karede ikisi de gelirse (kullanıcı iki hızlı tıklama) ikinci /// istek ilk diyalog bitene kadar bekletilir; aksi halde iki `AlertDialog` /// üst üste açılıp Flutter'da "Looking up a deactivated widget's ancestor" /// hatası verir. class _TileRenameBridge extends StatefulWidget { final Widget child; const _TileRenameBridge({required this.child}); @override State<_TileRenameBridge> createState() => _TileRenameBridgeState(); } class _TileRenameBridgeState extends State<_TileRenameBridge> { /// ⏳ Diyalog açıkken gelen istekler burada sıralanır (üst üste açılmaz). final List _pendingTitles = []; /// 🖥 Masaüstü adı istekleri (ayrı kuyruk: farklı diyalog, farklı hedef). final List _pendingDesktops = []; /// 🪟 Bir diyalog şu anda açık mı? (tek akış kuralı) bool _busy = false; /// 🔄 Notifier'ı temizlerken yeniden tetiklenmeyi engeller. /// /// `ValueNotifier.value = null` da BİR bildirim üretir → dinleyici /// (`_onTitle`) tekrar çağrılır. `null` zaten geldiği için elenir, ama /// kuyruğa ikinci kez eklenmesini kesin olarak engellemek için bayrak /// kullanılır (davranış sınırına dayanmayan, açık bir koruma). bool _pumping = false; CatiaProvider? _provider; @override void didChangeDependencies() { super.didChangeDependencies(); final p = context.read(); if (identical(_provider, p)) return; // 🔄 Provider değişirse (pencere yeniden kuruldu) ESKİ dinleyicileri // çıkar, yenilerini bağla — aksi halde notifier'lar ölü provider'a // bağlı kalır ve ikinci bir köprü aynı isteği iki kez işler. _provider?.tileRenameRequest.removeListener(_onTitle); _provider?.tileDesktopRenameRequest.removeListener(_onDesktop); _provider = p; p.tileRenameRequest.addListener(_onTitle); p.tileDesktopRenameRequest.addListener(_onDesktop); } @override void dispose() { _provider?.tileRenameRequest.removeListener(_onTitle); _provider?.tileDesktopRenameRequest.removeListener(_onDesktop); super.dispose(); } void _onTitle() { final req = _provider?.tileRenameRequest.value; if (req == null) return; // 📌 REENTRANSANSLIK KORUMASI: Aşağıda notifier'ı `null`'a çekiyoruz; // `ValueNotifier` bunu da BİR DEĞİŞİM olarak yayımlar ve `_onTitle` // YENİDEN çağrılır. İkinci çağrıda `value` zaten `null` olduğu için // ilk satırda elenir → sonsuz döngü YOK. Ancak "null'a çekme" işlemini // kuyruğa EKLERKEN yanlışlıkla ikinci kez eklememek mümkündü; // `_pumping` bayrağı bunu kesin olarak engeller. if (_pumping) return; _pumping = true; try { _provider?.tileRenameRequest.value = null; } finally { _pumping = false; } _pendingTitles.add(req); _drainTitles(); } void _onDesktop() { final req = _provider?.tileDesktopRenameRequest.value; if (req == null) return; if (_pumping) return; _pumping = true; try { _provider?.tileDesktopRenameRequest.value = null; } finally { _pumping = false; } _pendingDesktops.add(req); _drainDesktops(); } Future _drainTitles() async { if (_busy || _pendingTitles.isEmpty) return; _busy = true; try { while (_pendingTitles.isNotEmpty) { if (!mounted) return; final req = _pendingTitles.removeAt(0); final provider = _provider; if (provider == null) return; // 📌 `current` = kartın GÖRÜNEN başlığı. Boş bırakılırsa // `renameInstance` özel adı silip varsayılana döner (aynı kural). await showRenameDialog( context, provider, req.instanceId, req.title, ); // 🗑️ Diyalog kapanınca notifier'ın KENDİSİ de temizlenir // (bridge dispose edilmişse güvenli olsun diye opsiyonel çağrı). } } finally { _busy = false; } } Future _drainDesktops() async { if (_busy || _pendingDesktops.isEmpty) return; _busy = true; try { while (_pendingDesktops.isNotEmpty) { if (!mounted) return; final req = _pendingDesktops.removeAt(0); final provider = _provider; if (provider == null) return; await showDesktopRenameDialog( context, provider, req.instanceId, req.current, ); } } finally { _busy = false; } } @override Widget build(BuildContext context) => widget.child; }