fix branch picker: escape to dismiss, error display, backdrop click
Branch picker now handles Escape key to dismiss, shows an × close button in the header, and displays error/empty states instead of silently showing nothing. Dialog backdrop is click-to-dismiss (the dialog content stops propagation so clicks inside don't close it). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:clide/widgets/widgets.dart';
|
import 'package:clide/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class GitStatusItem extends StatefulWidget {
|
class GitStatusItem extends StatefulWidget {
|
||||||
@@ -116,13 +117,22 @@ class _BranchPicker extends StatefulWidget {
|
|||||||
class _BranchPickerState extends State<_BranchPicker> {
|
class _BranchPickerState extends State<_BranchPicker> {
|
||||||
List<Map<String, Object?>> _branches = const [];
|
List<Map<String, Object?>> _branches = const [];
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
|
String? _error;
|
||||||
|
late final FocusNode _focus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_focus = FocusNode()..requestFocus();
|
||||||
unawaited(_load());
|
unawaited(_load());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _load() async {
|
Future<void> _load() async {
|
||||||
final r = await widget.ipc.request('git.branches');
|
final r = await widget.ipc.request('git.branches');
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -133,6 +143,8 @@ class _BranchPickerState extends State<_BranchPicker> {
|
|||||||
for (final b in (r.data['branches'] as List? ?? const []))
|
for (final b in (r.data['branches'] as List? ?? const []))
|
||||||
(b as Map).cast<String, Object?>(),
|
(b as Map).cast<String, Object?>(),
|
||||||
];
|
];
|
||||||
|
} else {
|
||||||
|
_error = r.error?.message ?? 'failed to load branches';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -142,53 +154,75 @@ class _BranchPickerState extends State<_BranchPicker> {
|
|||||||
widget.onDismiss();
|
widget.onDismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KeyEventResult _onKey(FocusNode node, KeyEvent event) {
|
||||||
|
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
widget.onDismiss();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = ClideTheme.of(context).surface;
|
final tokens = ClideTheme.of(context).surface;
|
||||||
return Container(
|
return Focus(
|
||||||
width: 320,
|
focusNode: _focus,
|
||||||
constraints: const BoxConstraints(maxHeight: 320),
|
onKeyEvent: _onKey,
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: tokens.dropdownBackground,
|
width: 320,
|
||||||
border: Border.all(color: tokens.dropdownBorder),
|
constraints: const BoxConstraints(maxHeight: 320),
|
||||||
borderRadius: BorderRadius.circular(6),
|
decoration: BoxDecoration(
|
||||||
),
|
color: tokens.dropdownBackground,
|
||||||
child: Column(
|
border: Border.all(color: tokens.dropdownBorder),
|
||||||
mainAxisSize: MainAxisSize.min,
|
borderRadius: BorderRadius.circular(6),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
),
|
||||||
children: [
|
child: Column(
|
||||||
Padding(
|
mainAxisSize: MainAxisSize.min,
|
||||||
padding: const EdgeInsets.all(12),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
child: ClideText(
|
children: [
|
||||||
'Switch branch',
|
Padding(
|
||||||
fontSize: clideFontCaption,
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
color: tokens.globalTextMuted,
|
child: Row(
|
||||||
fontFamily: clideMonoFamily,
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ClideText('Switch branch', fontSize: clideFontCaption, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => widget.onDismiss(),
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: ClideIcon(PhosphorIcons.xMark, size: 12, color: tokens.globalTextMuted),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
if (_loading)
|
||||||
if (_loading)
|
const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
|
||||||
const Padding(
|
if (_error != null)
|
||||||
padding: EdgeInsets.all(12),
|
Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true)),
|
||||||
child: ClideText('Loading…', muted: true),
|
if (!_loading && _error == null && _branches.isEmpty)
|
||||||
),
|
const Padding(padding: EdgeInsets.all(12), child: ClideText('No branches found.', muted: true)),
|
||||||
Flexible(
|
if (_branches.isNotEmpty)
|
||||||
child: ListView.builder(
|
Flexible(
|
||||||
shrinkWrap: true,
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.zero,
|
shrinkWrap: true,
|
||||||
itemCount: _branches.length,
|
padding: EdgeInsets.zero,
|
||||||
itemBuilder: (ctx, i) {
|
itemCount: _branches.length,
|
||||||
final b = _branches[i];
|
itemBuilder: (ctx, i) {
|
||||||
final name = b['name'] as String? ?? '';
|
final b = _branches[i];
|
||||||
final current = b['current'] as bool? ?? false;
|
final name = b['name'] as String? ?? '';
|
||||||
return _BranchRow(
|
final current = b['current'] as bool? ?? false;
|
||||||
name: name,
|
return _BranchRow(
|
||||||
current: current,
|
name: name,
|
||||||
onTap: current ? null : () => unawaited(_checkout(name)),
|
current: current,
|
||||||
);
|
onTap: current ? null : () => unawaited(_checkout(name)),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,10 +93,16 @@ class DialogHost extends StatelessWidget {
|
|||||||
final b = router.current;
|
final b = router.current;
|
||||||
if (b == null) return const SizedBox.shrink();
|
if (b == null) return const SizedBox.shrink();
|
||||||
return Positioned.fill(
|
return Positioned.fill(
|
||||||
child: ColoredBox(
|
child: GestureDetector(
|
||||||
color: backdropColor,
|
onTap: () => router.dismiss(),
|
||||||
child: Center(
|
child: ColoredBox(
|
||||||
child: b(ctx, router.dismiss),
|
color: backdropColor,
|
||||||
|
child: Center(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: b(ctx, router.dismiss),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user