action_buttons.dart 14.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'action_icons_theme.dart';
import 'button_style.dart';
import 'debug.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'material_localizations.dart';
import 'scaffold.dart';
import 'theme.dart';

abstract class _ActionButton extends StatelessWidget {
  /// Creates a Material Design icon button.
  const _ActionButton({
    super.key,
    this.color,
    required this.icon,
    required this.onPressed,
    this.style,
  });

  /// The icon to display inside the button.
  final Widget icon;

  /// The callback that is called when the button is tapped
  /// or otherwise activated.
  ///
  /// If this is set to null, the button will do a default action
  /// when it is tapped or activated.
  final VoidCallback? onPressed;

  /// The color to use for the icon.
  ///
  /// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
  /// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
  final Color? color;

  /// Customizes this icon button's appearance.
  ///
  /// The [style] is only used for Material 3 [IconButton]s. If [ThemeData.useMaterial3]
  /// is set to true, [style] is preferred for icon button customization, and any
  /// parameters defined in [style] will override the same parameters in [IconButton].
  ///
  /// Null by default.
  final ButtonStyle? style;

  /// This returns the appropriate tooltip text for this action button.
  String _getTooltip(BuildContext context);

  /// This is the default function that is called when [onPressed] is set
  /// to null.
  void _onPressedCallback(BuildContext context);

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterialLocalizations(context));
    return IconButton(
      icon: icon,
      style: style,
      color: color,
      tooltip: _getTooltip(context),
      onPressed: () {
        if (onPressed != null) {
          onPressed!();
        } else {
          _onPressedCallback(context);
        }
      },
    );
  }
}

typedef _ActionIconBuilderCallback = WidgetBuilder? Function(ActionIconThemeData? actionIconTheme);
typedef _ActionIconDataCallback = IconData Function(BuildContext context);
typedef _AndroidSemanticsLabelCallback = String Function(MaterialLocalizations materialLocalization);

class _ActionIcon extends StatelessWidget {
  const _ActionIcon({
    required this.iconBuilderCallback,
    required this.getIcon,
    required this.getAndroidSemanticsLabel,
  });

  final _ActionIconBuilderCallback iconBuilderCallback;
  final _ActionIconDataCallback getIcon;
  final _AndroidSemanticsLabelCallback getAndroidSemanticsLabel;

  @override
  Widget build(BuildContext context) {
    final ActionIconThemeData? actionIconTheme = ActionIconTheme.of(context);
    final WidgetBuilder? iconBuilder = iconBuilderCallback(actionIconTheme);
    if (iconBuilder != null) {
      return iconBuilder(context);
    }

    final IconData data = getIcon(context);
    final String? semanticsLabel;
    // This can't use the platform from Theme because it is the Android OS that
    // expects the duplicated tooltip and label.
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        semanticsLabel = getAndroidSemanticsLabel(MaterialLocalizations.of(context));
        break;
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        semanticsLabel = null;
        break;
    }

    return Icon(data, semanticLabel: semanticsLabel);
  }
}

/// A "back" icon that's appropriate for the current [TargetPlatform].
///
/// The current platform is determined by querying for the ambient [Theme].
///
/// See also:
///
///  * [BackButton], an [IconButton] with a [BackButtonIcon] that calls
///    [Navigator.maybePop] to return to the previous route.
///  * [IconButton], which is a more general widget for creating buttons
///    with icons.
///  * [Icon], a Material Design icon.
///  * [ThemeData.platform], which specifies the current platform.
class BackButtonIcon extends StatelessWidget {
  /// Creates an icon that shows the appropriate "back" image for
  /// the current platform (as obtained from the [Theme]).
  const BackButtonIcon({ super.key });

  @override
  Widget build(BuildContext context) {
    return _ActionIcon(
      iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
        return actionIconTheme?.backButtonIconBuilder;
      },
      getIcon: (BuildContext context) {
        if (kIsWeb) {
          // Always use 'Icons.arrow_back' as a back_button icon in web.
          return Icons.arrow_back;
        }
        switch (Theme.of(context).platform) {
          case TargetPlatform.android:
          case TargetPlatform.fuchsia:
          case TargetPlatform.linux:
          case TargetPlatform.windows:
            return Icons.arrow_back;
          case TargetPlatform.iOS:
          case TargetPlatform.macOS:
            return Icons.arrow_back_ios;
        }
      },
      getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
        return materialLocalization.backButtonTooltip;
      },
    );
  }
}

/// A Material Design back icon button.
///
/// A [BackButton] is an [IconButton] with a "back" icon appropriate for the
/// current [TargetPlatform]. When pressed, the back button calls
/// [Navigator.maybePop] to return to the previous route unless a custom
/// [onPressed] callback is provided.
///
/// The [onPressed] callback can, for instance, be used to pop the platform's navigation stack
/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
/// situations.
///
/// In Material Design 3, both [style]'s [ButtonStyle.iconColor] and [color] are
/// used to override the default icon color of [BackButton]. If both exist, the [ButtonStyle.iconColor]
/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
///
/// When deciding to display a [BackButton], consider using
/// `ModalRoute.of(context)?.canPop` to check whether the current route can be
/// popped. If that value is false (e.g., because the current route is the
/// initial route), the [BackButton] will not have any effect when pressed,
/// which could frustrate the user.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
///  * [AppBar], which automatically uses a [BackButton] in its
///    [AppBar.leading] slot when the [Scaffold] has no [Drawer] and the
///    current [Route] is not the [Navigator]'s first route.
///  * [BackButtonIcon], which is useful if you need to create a back button
///    that responds differently to being pressed.
///  * [IconButton], which is a more general widget for creating buttons with
///    icons.
///  * [CloseButton], an alternative which may be more appropriate for leaf
///    node pages in the navigation tree.
class BackButton extends _ActionButton {
  /// Creates an [IconButton] with the appropriate "back" icon for the current
  /// target platform.
  const BackButton({
    super.key,
    super.color,
    super.style,
    super.onPressed,
  }) : super(icon: const BackButtonIcon());

  @override
  void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);

  @override
  String _getTooltip(BuildContext context) {
    return MaterialLocalizations.of(context).backButtonTooltip;
  }
}

/// A "close" icon that's appropriate for the current [TargetPlatform].
///
/// The current platform is determined by querying for the ambient [Theme].
///
/// See also:
///
///  * [CloseButton], an [IconButton] with a [CloseButtonIcon] that calls
///    [Navigator.maybePop] to return to the previous route.
///  * [IconButton], which is a more general widget for creating buttons
///    with icons.
///  * [Icon], a Material Design icon.
///  * [ThemeData.platform], which specifies the current platform.
class CloseButtonIcon extends StatelessWidget {
  /// Creates an icon that shows the appropriate "close" image for
  /// the current platform (as obtained from the [Theme]).
  const CloseButtonIcon({ super.key });

  @override
  Widget build(BuildContext context) {
    return _ActionIcon(
      iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
        return actionIconTheme?.closeButtonIconBuilder;
      },
      getIcon: (BuildContext context) => Icons.close,
      getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
        return materialLocalization.closeButtonTooltip;
      },
    );
  }
}

/// A Material Design close icon button.
///
/// A [CloseButton] is an [IconButton] with a "close" icon. When pressed, the
/// close button calls [Navigator.maybePop] to return to the previous route.
///
/// The [onPressed] callback can, for instance, be used to pop the platform's navigation stack
/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
/// situations.
///
/// In Material Design 3, both [style]'s [ButtonStyle.iconColor] and [color] are
/// used to override the default icon color of [CloseButton]. If both exist, the [ButtonStyle.iconColor]
/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
///
/// Use a [CloseButton] instead of a [BackButton] on fullscreen dialogs or
/// pages that may solicit additional actions to close.
///
/// See also:
///
///  * [AppBar], which automatically uses a [CloseButton] in its
///    [AppBar.leading] slot when appropriate.
///  * [BackButton], which is more appropriate for middle nodes in the
///    navigation tree or where pages can be popped instantaneously with
///    no user data consequence.
///  * [IconButton], to create other Material Design icon buttons.
class CloseButton extends _ActionButton {
  /// Creates a Material Design close icon button.
  const CloseButton({ super.key, super.color, super.onPressed, super.style })
      : super(icon: const CloseButtonIcon());

  @override
  void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);

  @override
  String _getTooltip(BuildContext context) {
    return MaterialLocalizations.of(context).closeButtonTooltip;
  }
}

/// A "drawer" icon that's appropriate for the current [TargetPlatform].
///
/// The current platform is determined by querying for the ambient [Theme].
///
/// See also:
///
///  * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
///    [ScaffoldState.openDrawer] to open the [Scaffold.drawer].
///  * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
///    calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
///  * [IconButton], which is a more general widget for creating buttons
///    with icons.
///  * [Icon], a Material Design icon.
///  * [ThemeData.platform], which specifies the current platform.
class DrawerButtonIcon extends StatelessWidget {
  /// Creates an icon that shows the appropriate "close" image for
  /// the current platform (as obtained from the [Theme]).
  const DrawerButtonIcon({ super.key });

  @override
  Widget build(BuildContext context) {
    return _ActionIcon(
      iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
        return actionIconTheme?.drawerButtonIconBuilder;
      },
      getIcon: (BuildContext context) => Icons.menu,
      getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
        return materialLocalization.openAppDrawerTooltip;
      },
    );
  }
}

/// A Material Design drawer icon button.
///
/// A [DrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the
/// close button calls [ScaffoldState.openDrawer] to the [Scaffold.drawer].
///
/// The default behaviour on press can be overriden with [onPressed].
///
/// See also:
///
///  * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
///    calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
///  * [IconButton], which is a more general widget for creating buttons
///    with icons.
///  * [Icon], a Material Design icon.
///  * [ThemeData.platform], which specifies the current platform.
class DrawerButton extends _ActionButton {
  /// Creates a Material Design drawer icon button.
  const DrawerButton({
    super.key,
    super.style,
    super.onPressed,
  }) : super(icon: const DrawerButtonIcon());

  @override
  void _onPressedCallback(BuildContext context) => Scaffold.of(context).openDrawer();

  @override
  String _getTooltip(BuildContext context) {
    return MaterialLocalizations.of(context).openAppDrawerTooltip;
  }
}

/// A "end drawer" icon that's appropriate for the current [TargetPlatform].
///
/// The current platform is determined by querying for the ambient [Theme].
///
/// See also:
///
///  * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
///    [ScaffoldState.openDrawer] to open the [Scaffold.drawer].
///  * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
///    calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer]
///  * [IconButton], which is a more general widget for creating buttons
///    with icons.
///  * [Icon], a Material Design icon.
///  * [ThemeData.platform], which specifies the current platform.
class EndDrawerButtonIcon extends StatelessWidget {
  /// Creates an icon that shows the appropriate "end drawer" image for
  /// the current platform (as obtained from the [Theme]).
  const EndDrawerButtonIcon({ super.key });

  @override
  Widget build(BuildContext context) {
    return _ActionIcon(
      iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
        return actionIconTheme?.endDrawerButtonIconBuilder;
      },
      getIcon: (BuildContext context) => Icons.menu,
      getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
        return materialLocalization.openAppDrawerTooltip;
      },
    );
  }
}

/// A Material Design end drawer icon button.
///
/// A [EndDrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the
/// end drawer button calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
///
/// The default behaviour on press can be overriden with [onPressed].
///
/// See also:
///
///  * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
///    [ScaffoldState.openDrawer] to open a drawer.
///  * [IconButton], which is a more general widget for creating buttons
///    with icons.
///  * [Icon], a Material Design icon.
///  * [ThemeData.platform], which specifies the current platform.
class EndDrawerButton extends _ActionButton {
  /// Creates a Material Design end drawer icon button.
  const EndDrawerButton({
    super.key,
    super.style,
    super.onPressed,
  }) : super(icon: const EndDrawerButtonIcon());

  @override
  void _onPressedCallback(BuildContext context) => Scaffold.of(context).openEndDrawer();

  @override
  String _getTooltip(BuildContext context) {
    return MaterialLocalizations.of(context).openAppDrawerTooltip;
  }
}