Grep-verified dead surface across shared widgets: the app-menu system's minWidth/maxWidth/childPadding/alignmentOffset knobs threaded through four layers with no caller ever passing them; FocusableListTile's suppressInitialSelect/hoverColor/textColor/iconColor machinery (~55 lines) reachable by no call site; OptimizedMediaImage.playlist with zero constructions; TvSpotlightBackground's primary-action button that neither caller can render; MediaGridDelegate.createDelegate whose only caller was a test (MediaGridGeometry.resolve is the single delegate composition now); and the CustomAppBar->Config->DesktopTopBar chain collapsed onto DesktopSliverAppBar with its ~11 never-passed passthrough params (snap kept — real callers pass it).
326 lines
9.4 KiB
Dart
326 lines
9.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../focus/focusable_tile_mixin.dart';
|
|
import '../utils/platform_detector.dart';
|
|
import 'clickable_cursor.dart';
|
|
|
|
/// A ListTile that accepts a FocusNode for keyboard/controller navigation.
|
|
///
|
|
/// Uses Flutter's native ListTile focus support - no custom styling wrapper.
|
|
/// The focusNode allows programmatic focus control (e.g., auto-focus first item).
|
|
class FocusableListTile extends StatefulWidget {
|
|
final Widget? title;
|
|
|
|
final Widget? subtitle;
|
|
|
|
final Widget? leading;
|
|
|
|
final Widget? trailing;
|
|
|
|
final VoidCallback? onTap;
|
|
|
|
final VoidCallback? onLongPress;
|
|
|
|
final bool dense;
|
|
|
|
final bool enabled;
|
|
|
|
final bool selected;
|
|
|
|
/// Optional FocusNode for keyboard/controller navigation.
|
|
final FocusNode? focusNode;
|
|
|
|
final bool autofocus;
|
|
|
|
final EdgeInsetsGeometry? contentPadding;
|
|
|
|
final VisualDensity? visualDensity;
|
|
|
|
final double? horizontalTitleGap;
|
|
|
|
final double? minLeadingWidth;
|
|
|
|
const FocusableListTile({
|
|
super.key,
|
|
this.title,
|
|
this.subtitle,
|
|
this.leading,
|
|
this.trailing,
|
|
this.onTap,
|
|
this.onLongPress,
|
|
this.dense = true,
|
|
this.enabled = true,
|
|
this.selected = false,
|
|
this.focusNode,
|
|
this.autofocus = false,
|
|
this.contentPadding,
|
|
this.visualDensity = const VisualDensity(vertical: -3),
|
|
this.horizontalTitleGap,
|
|
this.minLeadingWidth,
|
|
});
|
|
|
|
@override
|
|
State<FocusableListTile> createState() => _FocusableListTileState();
|
|
}
|
|
|
|
class _FocusableListTileState extends State<FocusableListTile> with FocusableTileStateMixin<FocusableListTile> {
|
|
@override
|
|
FocusNode? get widgetFocusNode => widget.focusNode;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final automotive = PlatformDetector.isAutomotive();
|
|
|
|
return MouseRegion(
|
|
cursor: widget.enabled && (widget.onTap != null || widget.onLongPress != null)
|
|
? SystemMouseCursors.click
|
|
: MouseCursor.defer,
|
|
child: ListTile(
|
|
title: widget.title,
|
|
subtitle: widget.subtitle,
|
|
leading: widget.leading,
|
|
trailing: widget.trailing,
|
|
onTap: widget.onTap,
|
|
onLongPress: widget.onLongPress,
|
|
dense: automotive ? false : widget.dense,
|
|
enabled: widget.enabled,
|
|
selected: widget.selected,
|
|
contentPadding: widget.contentPadding,
|
|
visualDensity: automotive ? VisualDensity.standard : widget.visualDensity,
|
|
focusNode: effectiveFocusNode,
|
|
autofocus: widget.autofocus,
|
|
horizontalTitleGap: widget.horizontalTitleGap,
|
|
minLeadingWidth: widget.minLeadingWidth,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A RadioListTile that accepts a FocusNode for keyboard/controller navigation.
|
|
///
|
|
/// Uses Flutter's native RadioListTile focus support - no custom styling wrapper.
|
|
/// Requires a [RadioGroup] ancestor to manage selection state.
|
|
class FocusableRadioListTile<T> extends StatefulWidget {
|
|
/// The primary content of the list tile.
|
|
final Widget? title;
|
|
|
|
/// Additional content displayed below the title.
|
|
final Widget? subtitle;
|
|
|
|
/// A widget to display on the opposite side from the radio.
|
|
final Widget? secondary;
|
|
|
|
/// The value represented by this radio button.
|
|
final T value;
|
|
|
|
/// Whether this radio button is part of a vertically dense list.
|
|
final bool dense;
|
|
|
|
/// Optional FocusNode for keyboard/controller navigation.
|
|
final FocusNode? focusNode;
|
|
|
|
/// Whether this tile should autofocus when first built.
|
|
final bool autofocus;
|
|
|
|
/// Whether the radio tile is interactive.
|
|
final bool? enabled;
|
|
|
|
/// Visual density for the list tile.
|
|
final VisualDensity? visualDensity;
|
|
|
|
const FocusableRadioListTile({
|
|
super.key,
|
|
this.title,
|
|
this.subtitle,
|
|
this.secondary,
|
|
required this.value,
|
|
this.dense = true,
|
|
this.focusNode,
|
|
this.autofocus = false,
|
|
this.enabled,
|
|
this.visualDensity = const VisualDensity(vertical: -3),
|
|
});
|
|
|
|
@override
|
|
State<FocusableRadioListTile<T>> createState() => _FocusableRadioListTileState<T>();
|
|
}
|
|
|
|
class _FocusableRadioListTileState<T> extends State<FocusableRadioListTile<T>>
|
|
with FocusableTileStateMixin<FocusableRadioListTile<T>> {
|
|
@override
|
|
FocusNode? get widgetFocusNode => widget.focusNode;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final automotive = PlatformDetector.isAutomotive();
|
|
return ClickableCursor(
|
|
enabled: widget.enabled ?? true,
|
|
child: RadioListTile<T>(
|
|
title: widget.title,
|
|
subtitle: widget.subtitle,
|
|
secondary: widget.secondary,
|
|
value: widget.value,
|
|
// groupValue and onChanged provided by RadioGroup ancestor
|
|
dense: automotive ? false : widget.dense,
|
|
visualDensity: automotive ? VisualDensity.standard : widget.visualDensity,
|
|
focusNode: effectiveFocusNode,
|
|
autofocus: widget.autofocus,
|
|
enabled: widget.enabled,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A SwitchListTile that accepts a FocusNode for keyboard/controller navigation.
|
|
///
|
|
/// Uses Flutter's native SwitchListTile focus support - no custom styling wrapper.
|
|
class FocusableSwitchListTile extends StatefulWidget {
|
|
/// The primary content of the list tile.
|
|
final Widget? title;
|
|
|
|
/// Additional content displayed below the title.
|
|
final Widget? subtitle;
|
|
|
|
/// A widget to display on the opposite side from the switch.
|
|
final Widget? secondary;
|
|
|
|
/// Whether this switch is checked.
|
|
final bool value;
|
|
|
|
/// Called when the user toggles the switch.
|
|
final ValueChanged<bool>? onChanged;
|
|
|
|
/// Whether this switch is part of a vertically dense list.
|
|
final bool dense;
|
|
|
|
/// Optional FocusNode for keyboard/controller navigation.
|
|
final FocusNode? focusNode;
|
|
|
|
/// Whether this tile should autofocus when first built.
|
|
final bool autofocus;
|
|
|
|
/// Visual density for the list tile.
|
|
final VisualDensity? visualDensity;
|
|
|
|
/// Content padding, e.g. to align with sibling rows. Null uses the
|
|
/// SwitchListTile default.
|
|
final EdgeInsetsGeometry? contentPadding;
|
|
|
|
/// Horizontal gap between the leading/secondary widget and title.
|
|
final double? horizontalTitleGap;
|
|
|
|
/// Minimum width reserved for the leading/secondary widget.
|
|
final double? minLeadingWidth;
|
|
|
|
const FocusableSwitchListTile({
|
|
super.key,
|
|
this.title,
|
|
this.subtitle,
|
|
this.secondary,
|
|
required this.value,
|
|
required this.onChanged,
|
|
this.dense = true,
|
|
this.focusNode,
|
|
this.autofocus = false,
|
|
this.visualDensity = const VisualDensity(vertical: -3),
|
|
this.contentPadding,
|
|
this.horizontalTitleGap,
|
|
this.minLeadingWidth,
|
|
});
|
|
|
|
@override
|
|
State<FocusableSwitchListTile> createState() => _FocusableSwitchListTileState();
|
|
}
|
|
|
|
class _FocusableSwitchListTileState extends State<FocusableSwitchListTile>
|
|
with FocusableTileStateMixin<FocusableSwitchListTile> {
|
|
@override
|
|
FocusNode? get widgetFocusNode => widget.focusNode;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final automotive = PlatformDetector.isAutomotive();
|
|
return ClickableCursor(
|
|
enabled: widget.onChanged != null,
|
|
child: SwitchListTile(
|
|
title: widget.title,
|
|
subtitle: widget.subtitle,
|
|
secondary: widget.secondary,
|
|
value: widget.value,
|
|
onChanged: widget.onChanged,
|
|
dense: automotive ? false : widget.dense,
|
|
visualDensity: automotive ? VisualDensity.standard : widget.visualDensity,
|
|
contentPadding: widget.contentPadding,
|
|
focusNode: effectiveFocusNode,
|
|
autofocus: widget.autofocus,
|
|
horizontalTitleGap: widget.horizontalTitleGap,
|
|
minLeadingWidth: widget.minLeadingWidth,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A CheckboxListTile that accepts a FocusNode for keyboard/controller navigation.
|
|
///
|
|
/// Uses Flutter's native CheckboxListTile focus support - no custom styling wrapper.
|
|
class FocusableCheckboxListTile extends StatefulWidget {
|
|
final Widget? title;
|
|
final Widget? subtitle;
|
|
final Widget? secondary;
|
|
final bool? value;
|
|
final ValueChanged<bool?>? onChanged;
|
|
final bool tristate;
|
|
final bool dense;
|
|
final FocusNode? focusNode;
|
|
final bool autofocus;
|
|
final VisualDensity? visualDensity;
|
|
final EdgeInsetsGeometry? contentPadding;
|
|
final ListTileControlAffinity controlAffinity;
|
|
|
|
const FocusableCheckboxListTile({
|
|
super.key,
|
|
this.title,
|
|
this.subtitle,
|
|
this.secondary,
|
|
required this.value,
|
|
required this.onChanged,
|
|
this.tristate = false,
|
|
this.dense = true,
|
|
this.focusNode,
|
|
this.autofocus = false,
|
|
this.visualDensity = const VisualDensity(vertical: -3),
|
|
this.contentPadding,
|
|
this.controlAffinity = ListTileControlAffinity.platform,
|
|
});
|
|
|
|
@override
|
|
State<FocusableCheckboxListTile> createState() => _FocusableCheckboxListTileState();
|
|
}
|
|
|
|
class _FocusableCheckboxListTileState extends State<FocusableCheckboxListTile>
|
|
with FocusableTileStateMixin<FocusableCheckboxListTile> {
|
|
@override
|
|
FocusNode? get widgetFocusNode => widget.focusNode;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final automotive = PlatformDetector.isAutomotive();
|
|
return ClickableCursor(
|
|
enabled: widget.onChanged != null,
|
|
child: CheckboxListTile(
|
|
title: widget.title,
|
|
subtitle: widget.subtitle,
|
|
secondary: widget.secondary,
|
|
value: widget.value,
|
|
onChanged: widget.onChanged,
|
|
tristate: widget.tristate,
|
|
dense: automotive ? false : widget.dense,
|
|
visualDensity: automotive ? VisualDensity.standard : widget.visualDensity,
|
|
contentPadding: widget.contentPadding,
|
|
focusNode: effectiveFocusNode,
|
|
autofocus: widget.autofocus,
|
|
controlAffinity: widget.controlAffinity,
|
|
),
|
|
);
|
|
}
|
|
}
|