// 🧰 Komut paleti penceresinin ekranı (alt engine tarafı) — D9 (4). // // Bu pencerenin stili/konumu/görünürlüğü TAMAMEN native'dedir // (windows/runner/palette_window.cpp): çerçevesiz (WS_POPUP), // NOACTIVATE/TOOLWINDOW/TOPMOST → **odak çalmaz**, Alt-Tab'da görünmez. // Arayüz yalnız mesaj alışverişi yapar: // ana → alt : `load`/`state` (StatePayload), `ping` // alt → ana : `ready`, `run` (öğe id), `drag`, `toggle`, `hide`, `anchor`, // `lock` // // ⚠ NOACTIVATE pencerede KLAVYE GİRİŞİ YOKTUR → bu ekranda metin alanı // bulunmaz (I6). Düzenleme popup'ta/Ayarlar'da yapılır. import 'dart:async'; import 'dart:math' as math; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart' show PointerDeviceKind; import 'package:flutter/material.dart'; import '../services/palette_model.dart'; import '../services/palette_service.dart'; import '../services/palette_state.dart'; import '../theme/app_theme.dart'; import '../widgets/mini_pill.dart'; // --------------------------------------------------------------------------- // Alt pencere ↔ ana pencere bağlantısı // --------------------------------------------------------------------------- class PaletteChildLink { /// Ana pencerenin son gönderdiği durum (null = henüz gelmedi). static final ValueNotifier data = ValueNotifier(null); static WindowController? _controller; static const _mainChannel = WindowMethodChannel( PaletteService.mainChannelName, mode: ChannelMode.unidirectional, ); /// `runApp`'ten ÖNCE çağrılır: ana pencere `state` gönderdiğinde arayüz /// henüz çizilmemiş (pencere gizli açılıyor) olsa bile veri kaybolmasın. static Future init(WindowController controller) async { _controller = controller; try { await controller .setWindowMethodHandler((call) async { switch (call.method) { case 'ping': return true; case 'state': data.value = PaletteStatePayload.fromJson(call.arguments); return true; } return null; }) .timeout(const Duration(seconds: 4)); } catch (e) { debugPrint('PALET-CHILD: setWindowMethodHandler HATA: $e'); } try { await _mainChannel .invokeMethod('ready') .timeout(const Duration(seconds: 4)); debugPrint('PALET-CHILD: ready ONAYLANDI'); } catch (e) { debugPrint('PALET-CHILD: ready BAŞARISIZ: $e'); } } static Future send(String method, [Object? args]) async { try { await _mainChannel.invokeMethod(method, args); } catch (e) { debugPrint('PALET-CHILD: $method gönderilemedi: $e'); } } /// ✕: pencereyi gizle. `desktop_multi_window` 0.3.1'de gerçek kapatma /// API'si yok (bkz. detail_window_screen) — kalıcı ayarı ana pencere yazar. static Future hideSelf() async { try { await _controller?.hide().timeout(const Duration(milliseconds: 900)); } catch (e) { debugPrint('PALET-CHILD: pencere gizlenemedi: $e'); } } } // --------------------------------------------------------------------------- // Uygulama kökü // --------------------------------------------------------------------------- class PaletteApp extends StatelessWidget { const PaletteApp({super.key}); @override Widget build(BuildContext context) { return AppThemeBuilder( builder: (context, theme) => MaterialApp( title: PaletteService.windowTitle, debugShowCheckedModeBanner: false, // Cam efektsiz pencere: yarı saydam `AppColors.bg` masaüstüyle // karışıp okunaksız olurdu (bkz. settings_screen.dart notu). theme: theme.copyWith(scaffoldBackgroundColor: AppColors.opaqueBg), home: const PaletteScreen(), ), ); } } // --------------------------------------------------------------------------- // Palet ekranı // --------------------------------------------------------------------------- class PaletteScreen extends StatefulWidget { const PaletteScreen({super.key}); @override State createState() => _PaletteScreenState(); } class _PaletteScreenState extends State { /// Son tıklanan öğe (kısa "gönderildi" parıltısı). Palette klavye olmadığı /// için görsel geri bildirim tek geri bildirim yolumuzdur. String? _flashedId; Timer? _flashTimer; @override void dispose() { _flashTimer?.cancel(); super.dispose(); } void _run(String id, bool enabled) { if (!enabled) return; unawaited(PaletteChildLink.send('run', id)); _flashTimer?.cancel(); setState(() => _flashedId = id); _flashTimer = Timer(const Duration(milliseconds: 700), () { if (mounted) setState(() => _flashedId = null); }); } Future _hide() async { await PaletteChildLink.send('hide'); await PaletteChildLink.hideSelf(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.opaqueBg, body: ValueListenableBuilder( valueListenable: PaletteChildLink.data, builder: (context, data, _) { final entries = data?.entries ?? const []; return Stack( children: [ Container( decoration: BoxDecoration( border: Border.all(color: AppColors.border), borderRadius: BorderRadius.circular(6), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _Header( data: data, onCycleTarget: () => PaletteChildLink.send('target'), onToggleShowMode: () => PaletteChildLink.send('showMode'), onToggleViewMode: () => PaletteChildLink.send('toggle'), onLock: () => PaletteChildLink.send('lock'), onAnchor: () => PaletteChildLink.send('anchor'), onHide: _hide, ), Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(6, 4, 6, 6), child: entries.isEmpty ? _Empty(data) : _Entries( data: data!, entries: entries, flashedId: _flashedId, onRun: _run, ), ), ), ], ), ), // 🧰 Boyutlandırma tutamaçları (yeni, ana pencere gibi): sağ // kenar + alt kenar + sağ-alt köşe. Flutter'ın çocuk HWND'si // TÜM istemci alanını kapladığı için native WM_NCHITTEST resize // ÇALIŞMAZ — ⠿ sürükleme tutamacıyla AYNI desen: `onPointerDown` // native taraftan boyutlandırma döngüsünü başlatır (I7-c). const _ResizeHandle(edge: 1, right: true), const _ResizeHandle(edge: 2, bottom: true), const _ResizeHandle(edge: 3, right: true, bottom: true), ], ); }, ), ); } } /// 🧰 Boyutlandırma tutamacı (yeni): görünmez ince şerit/köşe. [edge] native /// `beginPaletteResize`e AYNEN iletilir (1=sağ, 2=alt, 3=sağ-alt köşe — bkz. /// `PaletteResizeEdge`, windows/runner/palette_window.h). Konum: [right]/ /// [bottom] true ise ilgili kenara/köşeye yaslanır. class _ResizeHandle extends StatelessWidget { final int edge; final bool right; final bool bottom; const _ResizeHandle({ required this.edge, this.right = false, this.bottom = false, }); static const _thickness = 6.0; static const _corner = 12.0; @override Widget build(BuildContext context) { final isCorner = right && bottom; final cursor = isCorner ? SystemMouseCursors.resizeUpLeftDownRight : (right ? SystemMouseCursors.resizeLeftRight : SystemMouseCursors.resizeUpDown); final Widget positioned; if (isCorner) { // Sağ-alt köşe: küçük kare, yalnız [right]/[bottom]'a yaslanır. positioned = Positioned( right: 0, bottom: 0, width: _corner, height: _corner, child: _handle(cursor), ); } else if (right) { // Sağ kenar: tam yükseklik, ince dikey şerit. positioned = Positioned( top: 0, bottom: 0, right: 0, width: _thickness, child: _handle(cursor), ); } else { // Alt kenar: tam genişlik, ince yatay şerit. positioned = Positioned( left: 0, right: 0, bottom: 0, height: _thickness, child: _handle(cursor), ); } return positioned; } Widget _handle(MouseCursor cursor) => MouseRegion( cursor: cursor, child: Listener( onPointerDown: (_) => PaletteChildLink.send('resize', edge), behavior: HitTestBehavior.translucent, child: const SizedBox.expand(), ), ); } /// Başlık şeridi: ⠿ sürükleme tutamacı + hedef/bağlam + görünüm/📌/⚓/✕. class _Header extends StatelessWidget { final PaletteStatePayload? data; final VoidCallback onCycleTarget; final VoidCallback onToggleShowMode; final VoidCallback onToggleViewMode; final VoidCallback onLock; final VoidCallback onAnchor; final Future Function() onHide; const _Header({ required this.data, required this.onCycleTarget, required this.onToggleShowMode, required this.onToggleViewMode, required this.onLock, required this.onAnchor, required this.onHide, }); @override Widget build(BuildContext context) { final d = data; final emojiLabel = d?.viewMode != PaletteViewMode.emojiOnly; final locked = d?.locked == true; final ctxText = [ if ((d?.targetTitle ?? '').isNotEmpty) d!.targetTitle, // 🧰 Ham kimlik ("PrtCfg") yerine okunaklı ad ("Part Design") — bkz. // palette_model.dart paletteWorkbenchDisplayName. if ((d?.workbenchId ?? '').isNotEmpty) paletteWorkbenchDisplayName(d!.workbenchId), if ((d?.docKind ?? '').isNotEmpty) d!.docKind, ].join(' · '); return Container( height: 20, padding: const EdgeInsets.only(left: 2, right: 2), decoration: BoxDecoration( color: AppColors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(6)), border: Border(bottom: BorderSide(color: AppColors.border)), ), child: Row( children: [ // ⠿ Tutamaç: basılınca native sürükleme döngüsü devralır (I7-c) — // Dart yalnız "tuttum" der, konum kanaldan GEÇMEZ. Tooltip( message: locked ? 'Konum kilitli (📌) — önce kilidi aç' : 'Sürükle', child: Listener( onPointerDown: (_) => PaletteChildLink.send('drag'), child: SizedBox( width: 18, height: 20, child: Center( child: Text( '⠿', style: TextStyle( fontSize: 12, height: 1.0, color: locked ? AppColors.textMuted : AppColors.textSecondary, ), ), ), ), ), ), const SizedBox(width: 3), // 🎯 Hedef: hangi CATIA'ya gideceği burada YAZAR. Elle seçilebilir: // tıkla → sıradaki bağlı CATIA (sonunda otomatik/ön plan takibine döner). Expanded( child: Tooltip( message: 'Hedef CATIA — tıkla: sıradaki bağlı CATIA' '${data?.targetPinned == true ? ' (şu an 🎯 elle seçili)' : ''}', waitDuration: const Duration(milliseconds: 400), child: InkWell( onTap: onCycleTarget, borderRadius: BorderRadius.circular(3), child: Padding( padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 2), child: Text( ctxText.isEmpty ? '🧰 Komut paleti' : (data?.targetPinned == true ? '🎯 $ctxText' : ctxText), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: data?.targetPinned == true ? AppColors.accent : AppColors.textSecondary, fontSize: 9, fontWeight: FontWeight.w600, ), ), ), ), ), ), _Chrome( emoji: (d?.showMode ?? PaletteShowMode.always) == PaletteShowMode.whenCatia ? '👁' : '🌐', tooltip: (d?.showMode ?? PaletteShowMode.always) == PaletteShowMode.whenCatia ? 'Mod: yalnız CATIA/biz öndeyken (tıkla: her zaman görünür)' : 'Mod: her zaman görünür (tıkla: yalnız CATIA öndeyken)', onTap: onToggleShowMode, ), _Chrome( emoji: emojiLabel ? '🏷' : '🔤', tooltip: emojiLabel ? 'Görünüm: emoji + etiket (tıkla: yalnız emoji)' : 'Görünüm: yalnız emoji (tıkla: emoji + etiket)', onTap: onToggleViewMode, ), _Chrome( emoji: '📌', tooltip: locked ? 'Konum kilidi AÇIK' : 'Konumu kilitle', color: locked ? AppColors.accent : null, onTap: onLock, ), _Chrome( emoji: '⚓', tooltip: 'Otomatik yerleşime dön (popup üstü / sağ alt)', onTap: onAnchor, ), _Chrome( emoji: '✕', tooltip: 'Paleti gizle', onTap: () => unawaited(onHide()), ), ], ), ); } } /// Başlıktaki 18×18 kare düğme (mini_toolbar görsel diliyle aynı). class _Chrome extends StatelessWidget { final String emoji; final String tooltip; final VoidCallback onTap; final Color? color; const _Chrome({ required this.emoji, required this.tooltip, required this.onTap, this.color, }); @override Widget build(BuildContext context) { return Tooltip( message: tooltip, waitDuration: const Duration(milliseconds: 400), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(3), child: SizedBox( width: 18, height: 18, child: Center( child: Text( emoji, style: TextStyle( fontSize: 10, height: 1.0, color: color, fontFamilyFallback: kEmojiFontFallback, ), ), ), ), ), ); } } /// Öğe yoksa nedeni: hedef CATIA yok / bu bağlama öğe eklenmemiş. class _Empty extends StatelessWidget { final PaletteStatePayload? data; const _Empty(this.data); @override Widget build(BuildContext context) { final d = data; final text = d == null ? 'bağlantı bekleniyor…' : (d.note.isNotEmpty ? d.note : 'bu bağlam için palete öğe eklenmemiş'); return Center( child: Text( '🧰 $text', textAlign: TextAlign.center, style: TextStyle(color: AppColors.textMuted, fontSize: 9.5, height: 1.3), ), ); } } /// Görünür öğeler: küçük kareler halinde YAN YANA ızgara (grid). Pencere /// genişliğine göre sütun sayısı kendiliğinden ayarlanır; taşarsa dikey kayar /// ve uçlarda "lastik" (bouncing) efektiyle geri sekerek durur. Mouse ile de /// sürükleyerek kaydırılabilir (masaüstünde dokunmatik gibi). class _Entries extends StatefulWidget { final PaletteStatePayload data; final List entries; final String? flashedId; final void Function(String id, bool enabled) onRun; const _Entries({ required this.data, required this.entries, required this.flashedId, required this.onRun, }); @override State<_Entries> createState() => _EntriesState(); } class _EntriesState extends State<_Entries> { final ScrollController _scroll = ScrollController(); /// Yerel (iyimser) sıra: sürükleme sırasında ANINDA görsel geri bildirim /// için tutulur. Ana pencereden yeni bir `state` gelince (kimlik KÜMESİ ya /// da SIRASI değiştiyse) buradan senkronlanır — böylece gerçek kaynak /// (`_macros`, tek yazıcı: CatiaProvider) her zaman kazanır; sürükleme /// SIRASINDA gelen bir push (ör. seçim değişikliği) yerel sırayı bozmaz. late List _order; @override void initState() { super.initState(); _order = List.of(widget.entries); } @override void didUpdateWidget(covariant _Entries old) { super.didUpdateWidget(old); if (!_sameOrder(_order, widget.entries)) { _order = List.of(widget.entries); } } bool _sameOrder(List a, List b) { if (a.length != b.length) return false; for (var i = 0; i < a.length; i++) { if (a[i].id != b[i].id) return false; } return true; } @override void dispose() { _scroll.dispose(); super.dispose(); } /// [draggedId] bırakıldığı yer: [targetId]'nin ÖNÜNE geçer. Yerel sırayı /// hemen günceller (iyimser) ve yeni sırayı ana pencereye bildirir — asıl /// kayıt/yeniden dağıtım orada olur (bkz. CatiaProvider.reorderPaletteEntries). void _onReorder(String draggedId, String targetId) { if (draggedId == targetId) return; final list = List.of(_order); final fromIndex = list.indexWhere((e) => e.id == draggedId); final toIndex = list.indexWhere((e) => e.id == targetId); if (fromIndex < 0 || toIndex < 0) return; final item = list.removeAt(fromIndex); list.insert(toIndex, item); setState(() => _order = list); unawaited( PaletteChildLink.send('reorder', [for (final e in list) e.id])); } @override Widget build(BuildContext context) { final showLabel = widget.data.viewMode == PaletteViewMode.emojiLabel; final cellWidth = showLabel ? 62.0 : 30.0; final cellHeight = showLabel ? 38.0 : 28.0; final behavior = ScrollConfiguration.of(context).copyWith( scrollbars: false, // kendi ince çubuğumuzu kullanıyoruz physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics()), dragDevices: const { PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.trackpad, PointerDeviceKind.stylus, }, ); return ScrollConfiguration( behavior: behavior, child: Scrollbar( controller: _scroll, thickness: 3, radius: const Radius.circular(2), child: GridView( controller: _scroll, padding: const EdgeInsets.only(right: 5, bottom: 2), gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: cellWidth, mainAxisExtent: cellHeight, crossAxisSpacing: 4, mainAxisSpacing: 4, ), children: [ for (var i = 0; i < _order.length; i++) _DraggableEntry( key: ValueKey(_order[i].id), entry: _order[i], index: i, showLabel: showLabel, cellWidth: cellWidth, cellHeight: cellHeight, flashed: widget.flashedId == _order[i].id, onTap: () => widget.onRun(_order[i].id, _order[i].enabled), onReorder: _onReorder, ), ], ), ), ); } } /// Tek palet karesini sürükle-bırak ile yeniden sıralanabilir yapan sarmalayıcı: /// AYNI zamanda bir [Draggable] (kendini taşır) ve bir [DragTarget] (üstüne /// bırakılanı kabul eder). Mouse ile: basılı tutup HAREKET ettirince sürükleme /// başlar (basit tıklama `onTap`'i etkilemez — Flutter jest arenası hareket /// olmadan tıklamayı kazandırır). class _DraggableEntry extends StatelessWidget { final PaletteEntryView entry; final int index; final bool showLabel; final double cellWidth; final double cellHeight; final bool flashed; final VoidCallback onTap; final void Function(String draggedId, String targetId) onReorder; const _DraggableEntry({ super.key, required this.entry, required this.index, required this.showLabel, required this.cellWidth, required this.cellHeight, required this.flashed, required this.onTap, required this.onReorder, }); @override Widget build(BuildContext context) { final button = _EntryButton( entry: entry, index: index, showLabel: showLabel, flashed: flashed, onTap: onTap, ); return DragTarget( onWillAcceptWithDetails: (details) => details.data != entry.id, onAcceptWithDetails: (details) => onReorder(details.data, entry.id), builder: (context, candidateData, rejectedData) { final hovered = candidateData.isNotEmpty; final target = AnimatedContainer( duration: const Duration(milliseconds: 100), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), border: hovered ? Border.all(color: AppColors.accent, width: 2) : null, ), child: button, ); return Draggable( data: entry.id, feedback: _DragFeedback( entry: entry, width: cellWidth, height: cellHeight), childWhenDragging: Opacity(opacity: 0.25, child: button), child: target, ); }, ); } } /// Sürüklerken imlecin altında görünen küçük önizleme. class _DragFeedback extends StatelessWidget { final PaletteEntryView entry; final double width; final double height; const _DragFeedback( {required this.entry, required this.width, required this.height}); @override Widget build(BuildContext context) { return Material( color: Colors.transparent, child: SizedBox( width: width, height: height, child: Container( decoration: BoxDecoration( color: AppColors.surfaceAlt, borderRadius: BorderRadius.circular(6), border: Border.all(color: AppColors.accent), boxShadow: [ BoxShadow( color: AppColors.accent.withValues(alpha: 0.35), blurRadius: 8, ), ], ), alignment: Alignment.center, child: Text( entry.emoji.isEmpty ? '▶' : entry.emoji, style: const TextStyle( fontSize: 13, height: 1.0, fontFamilyFallback: kEmojiFontFallback, ), ), ), ), ); } } /// Tek palet karesi. Hedef CATIA yoksa soluk çizilir ve tıklanamaz (ana taraf /// da `run`'ı reddeder — çift güvence). Efektler: ilk görünüşte yumuşak /// belirme (sıralı), üzerine gelince vurgu + hafif büyüme, basınca küçülme, /// çalıştırınca parıltı. class _EntryButton extends StatefulWidget { final PaletteEntryView entry; final int index; final bool showLabel; final bool flashed; final VoidCallback onTap; const _EntryButton({ super.key, required this.entry, required this.index, required this.showLabel, required this.flashed, required this.onTap, }); @override State<_EntryButton> createState() => _EntryButtonState(); } class _EntryButtonState extends State<_EntryButton> { bool _hover = false; bool _down = false; @override Widget build(BuildContext context) { final entry = widget.entry; final enabled = entry.enabled; final emoji = entry.emoji.isEmpty ? '▶' : entry.emoji; final label = entry.label.isEmpty ? entry.id : entry.label; final fg = enabled ? AppColors.textPrimary : AppColors.textMuted; final tip = entry.tooltip.isEmpty ? label : entry.tooltip; final hot = enabled && _hover; final borderColor = widget.flashed ? AppColors.accent : hot ? AppColors.accent.withValues(alpha: 0.7) : (enabled ? AppColors.border : AppColors.border.withValues(alpha: 0.5)); final fill = widget.flashed ? AppColors.accent.withValues(alpha: 0.22) : hot ? Color.alphaBlend(AppColors.accent.withValues(alpha: 0.10), AppColors.surfaceAlt) : AppColors.surfaceAlt; final scale = _down ? 0.90 : (hot ? 1.06 : 1.0); final face = AnimatedScale( scale: scale, duration: const Duration(milliseconds: 110), curve: Curves.easeOut, child: AnimatedContainer( duration: const Duration(milliseconds: 140), curve: Curves.easeOut, alignment: Alignment.center, decoration: BoxDecoration( color: fill, borderRadius: BorderRadius.circular(6), border: Border.all(color: borderColor), boxShadow: hot ? [ BoxShadow( color: AppColors.accent.withValues(alpha: 0.25), blurRadius: 6, ), ] : const [], ), child: widget.showLabel ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( emoji, style: const TextStyle( fontSize: 13, height: 1.0, fontFamilyFallback: kEmojiFontFallback, ), ), const SizedBox(height: 2), Padding( padding: const EdgeInsets.symmetric(horizontal: 3), child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle(color: fg, fontSize: 8.5, height: 1.0), ), ), ], ) : Text( emoji, style: const TextStyle( fontSize: 13, height: 1.0, fontFamilyFallback: kEmojiFontFallback, ), ), ), ); final interactive = MouseRegion( cursor: enabled ? SystemMouseCursors.click : SystemMouseCursors.basic, onEnter: (_) => setState(() => _hover = true), onExit: (_) => setState(() { _hover = false; _down = false; }), child: GestureDetector( behavior: HitTestBehavior.opaque, onTapDown: enabled ? (_) => setState(() => _down = true) : null, onTapUp: enabled ? (_) => setState(() => _down = false) : null, onTapCancel: enabled ? () => setState(() => _down = false) : null, onTap: enabled ? widget.onTap : null, child: face, ), ); // İlk görünüşte sıralı (kademeli) belirme; anahtar sabit olduğundan // yalnız YENİ öğelerde tekrar oynar. final delay = math.min(widget.index, 14) * 22; return Tooltip( message: enabled ? tip : '$tip (bağlı CATIA yok)', waitDuration: const Duration(milliseconds: 400), child: TweenAnimationBuilder( tween: Tween(begin: 0, end: 1), duration: Duration(milliseconds: 200 + delay), curve: Curves.easeOutCubic, builder: (context, t, child) => Opacity( opacity: t, child: Transform.translate( offset: Offset(0, (1 - t) * 6), child: child, ), ), child: interactive, ), ); } }