options.dart 15.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

import 'about.dart';
import 'scales.dart';

10
@immutable
11
class GalleryOptions {
12
  const GalleryOptions({
13
    this.themeMode,
14
    this.textScaleFactor,
15
    this.visualDensity,
16 17
    this.textDirection = TextDirection.ltr,
    this.timeDilation = 1.0,
18
    this.platform,
19 20 21
    this.showOffscreenLayersCheckerboard = false,
    this.showRasterCacheImagesCheckerboard = false,
    this.showPerformanceOverlay = false,
22 23
  });

24 25 26
  final ThemeMode? themeMode;
  final GalleryTextScaleValue? textScaleFactor;
  final GalleryVisualDensityValue? visualDensity;
27 28
  final TextDirection textDirection;
  final double timeDilation;
29
  final TargetPlatform? platform;
30 31 32 33 34
  final bool showPerformanceOverlay;
  final bool showRasterCacheImagesCheckerboard;
  final bool showOffscreenLayersCheckerboard;

  GalleryOptions copyWith({
35 36 37 38 39 40 41 42 43
    ThemeMode? themeMode,
    GalleryTextScaleValue? textScaleFactor,
    GalleryVisualDensityValue? visualDensity,
    TextDirection? textDirection,
    double? timeDilation,
    TargetPlatform? platform,
    bool? showPerformanceOverlay,
    bool? showRasterCacheImagesCheckerboard,
    bool? showOffscreenLayersCheckerboard,
44
  }) {
45
    return GalleryOptions(
46
      themeMode: themeMode ?? this.themeMode,
47
      textScaleFactor: textScaleFactor ?? this.textScaleFactor,
48
      visualDensity: visualDensity ?? this.visualDensity,
49 50 51 52 53 54 55 56 57 58
      textDirection: textDirection ?? this.textDirection,
      timeDilation: timeDilation ?? this.timeDilation,
      platform: platform ?? this.platform,
      showPerformanceOverlay: showPerformanceOverlay ?? this.showPerformanceOverlay,
      showOffscreenLayersCheckerboard: showOffscreenLayersCheckerboard ?? this.showOffscreenLayersCheckerboard,
      showRasterCacheImagesCheckerboard: showRasterCacheImagesCheckerboard ?? this.showRasterCacheImagesCheckerboard,
    );
  }

  @override
59 60
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
61
      return false;
62 63 64
    return other is GalleryOptions
        && other.themeMode == themeMode
        && other.textScaleFactor == textScaleFactor
65
        && other.visualDensity == visualDensity
66 67 68 69 70
        && other.textDirection == textDirection
        && other.platform == platform
        && other.showPerformanceOverlay == showPerformanceOverlay
        && other.showRasterCacheImagesCheckerboard == showRasterCacheImagesCheckerboard
        && other.showOffscreenLayersCheckerboard == showRasterCacheImagesCheckerboard;
71 72 73 74
  }

  @override
  int get hashCode => hashValues(
75
    themeMode,
76
    textScaleFactor,
77
    visualDensity,
78 79 80 81 82 83 84 85 86 87
    textDirection,
    timeDilation,
    platform,
    showPerformanceOverlay,
    showRasterCacheImagesCheckerboard,
    showOffscreenLayersCheckerboard,
  );

  @override
  String toString() {
88
    return '$runtimeType($themeMode)';
89 90 91 92
  }
}

const double _kItemHeight = 48.0;
93
const EdgeInsetsDirectional _kItemPadding = EdgeInsetsDirectional.only(start: 56.0);
94 95

class _OptionsItem extends StatelessWidget {
96
  const _OptionsItem({ Key? key, this.child }) : super(key: key);
97

98
  final Widget? child;
99 100 101

  @override
  Widget build(BuildContext context) {
102
    final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
103

104 105 106
    return MergeSemantics(
      child: Container(
        constraints: BoxConstraints(minHeight: _kItemHeight * textScaleFactor),
107 108
        padding: _kItemPadding,
        alignment: AlignmentDirectional.centerStart,
109
        child: DefaultTextStyle(
110 111 112
          style: DefaultTextStyle.of(context).style,
          maxLines: 2,
          overflow: TextOverflow.fade,
113
          child: IconTheme(
114
            data: Theme.of(context).primaryIconTheme,
115
            child: child!,
116
          ),
117 118 119 120 121 122 123
        ),
      ),
    );
  }
}

class _BooleanItem extends StatelessWidget {
124
  const _BooleanItem(this.title, this.value, this.onChanged, { this.switchKey });
125 126 127 128

  final String title;
  final bool value;
  final ValueChanged<bool> onChanged;
129
  // [switchKey] is used for accessing the switch from driver tests.
130
  final Key? switchKey;
131 132 133 134

  @override
  Widget build(BuildContext context) {
    final bool isDark = Theme.of(context).brightness == Brightness.dark;
135 136
    return _OptionsItem(
      child: Row(
137
        children: <Widget>[
138 139
          Expanded(child: Text(title)),
          Switch(
140
            key: switchKey,
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
            value: value,
            onChanged: onChanged,
            activeColor: const Color(0xFF39CEFD),
            activeTrackColor: isDark ? Colors.white30 : Colors.black26,
          ),
        ],
      ),
    );
  }
}

class _ActionItem extends StatelessWidget {
  const _ActionItem(this.text, this.onTap);

  final String text;
156
  final VoidCallback? onTap;
157 158 159

  @override
  Widget build(BuildContext context) {
160
    return _OptionsItem(
161
      child: _TextButton(
162
        onPressed: onTap,
163
        child: Text(text),
164 165 166 167 168
      ),
    );
  }
}

169
class _TextButton extends StatelessWidget {
170
  const _TextButton({ Key? key, this.onPressed, this.child }) : super(key: key);
171

172 173
  final VoidCallback? onPressed;
  final Widget? child;
174 175 176

  @override
  Widget build(BuildContext context) {
177 178 179 180 181 182
    final ThemeData theme = Theme.of(context);
    return TextButton(
      style: TextButton.styleFrom(
        primary: theme.colorScheme.onPrimary,
        textStyle: theme.textTheme.subtitle1,
        padding: EdgeInsets.zero,
183
      ),
184
      onPressed: onPressed,
185
      child: child!,
186 187 188 189 190 191 192 193 194 195 196 197
    );
  }
}

class _Heading extends StatelessWidget {
  const _Heading(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
198 199
    return _OptionsItem(
      child: DefaultTextStyle(
200
        style: theme.textTheme.headline6!.copyWith(
201
          fontFamily: 'GoogleSans',
202 203
          color: theme.colorScheme.onPrimary,
          fontWeight: FontWeight.w700,
204
        ),
205
        child: Semantics(
206
          header: true,
207
          child: Text(text),
208 209 210 211 212 213
        ),
      ),
    );
  }
}

214 215
class _ThemeModeItem extends StatelessWidget {
  const _ThemeModeItem(this.options, this.onOptionsChanged);
216

217 218
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
219

220 221 222 223 224 225
  static final Map<ThemeMode, String> modeLabels = <ThemeMode, String>{
    ThemeMode.system: 'System Default',
    ThemeMode.light: 'Light',
    ThemeMode.dark: 'Dark',
  };

226 227
  @override
  Widget build(BuildContext context) {
228 229 230 231 232 233 234 235 236
    return _OptionsItem(
      child: Row(
        children: <Widget>[
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text('Theme'),
                Text(
237
                  modeLabels[options!.themeMode!]!,
238
                  style: Theme.of(context).primaryTextTheme.bodyText2,
239 240 241
                ),
              ],
            ),
242
          ),
243 244 245
          PopupMenuButton<ThemeMode>(
            padding: const EdgeInsetsDirectional.only(end: 16.0),
            icon: const Icon(Icons.arrow_drop_down),
246
            initialValue: options!.themeMode,
247 248 249 250
            itemBuilder: (BuildContext context) {
              return ThemeMode.values.map<PopupMenuItem<ThemeMode>>((ThemeMode mode) {
                return PopupMenuItem<ThemeMode>(
                  value: mode,
251
                  child: Text(modeLabels[mode]!),
252 253 254 255
                );
              }).toList();
            },
            onSelected: (ThemeMode mode) {
256 257
              onOptionsChanged!(
                options!.copyWith(themeMode: mode),
258 259 260 261 262
              );
            },
          ),
        ],
      ),
263 264 265 266 267 268 269
    );
  }
}

class _TextScaleFactorItem extends StatelessWidget {
  const _TextScaleFactorItem(this.options, this.onOptionsChanged);

270 271
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
272 273 274

  @override
  Widget build(BuildContext context) {
275 276
    return _OptionsItem(
      child: Row(
277
        children: <Widget>[
278 279
          Expanded(
            child: Column(
280 281 282
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text('Text size'),
283
                Text(
284
                  options!.textScaleFactor!.label,
285
                  style: Theme.of(context).primaryTextTheme.bodyText2,
286 287 288 289
                ),
              ],
            ),
          ),
290
          PopupMenuButton<GalleryTextScaleValue>(
291 292 293
            padding: const EdgeInsetsDirectional.only(end: 16.0),
            icon: const Icon(Icons.arrow_drop_down),
            itemBuilder: (BuildContext context) {
294
              return kAllGalleryTextScaleValues.map<PopupMenuItem<GalleryTextScaleValue>>((GalleryTextScaleValue scaleValue) {
295
                return PopupMenuItem<GalleryTextScaleValue>(
296
                  value: scaleValue,
297
                  child: Text(scaleValue.label),
298 299 300 301
                );
              }).toList();
            },
            onSelected: (GalleryTextScaleValue scaleValue) {
302 303
              onOptionsChanged!(
                options!.copyWith(textScaleFactor: scaleValue),
304 305 306 307 308 309 310 311 312
              );
            },
          ),
        ],
      ),
    );
  }
}

313 314 315
class _VisualDensityItem extends StatelessWidget {
  const _VisualDensityItem(this.options, this.onOptionsChanged);

316 317
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
318 319 320 321 322 323 324 325 326 327 328 329

  @override
  Widget build(BuildContext context) {
    return _OptionsItem(
      child: Row(
        children: <Widget>[
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text('Visual density'),
                Text(
330
                  options!.visualDensity!.label,
331
                  style: Theme.of(context).primaryTextTheme.bodyText2,
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
                ),
              ],
            ),
          ),
          PopupMenuButton<GalleryVisualDensityValue>(
            padding: const EdgeInsetsDirectional.only(end: 16.0),
            icon: const Icon(Icons.arrow_drop_down),
            itemBuilder: (BuildContext context) {
              return kAllGalleryVisualDensityValues.map<PopupMenuItem<GalleryVisualDensityValue>>((GalleryVisualDensityValue densityValue) {
                return PopupMenuItem<GalleryVisualDensityValue>(
                  value: densityValue,
                  child: Text(densityValue.label),
                );
              }).toList();
            },
            onSelected: (GalleryVisualDensityValue densityValue) {
348 349
              onOptionsChanged!(
                options!.copyWith(visualDensity: densityValue),
350 351 352 353 354 355 356 357 358
              );
            },
          ),
        ],
      ),
    );
  }
}

359 360 361
class _TextDirectionItem extends StatelessWidget {
  const _TextDirectionItem(this.options, this.onOptionsChanged);

362 363
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
364 365 366

  @override
  Widget build(BuildContext context) {
367
    return _BooleanItem(
368
      'Force RTL',
369
      options!.textDirection == TextDirection.rtl,
370
      (bool value) {
371 372
        onOptionsChanged!(
          options!.copyWith(
373 374 375 376
            textDirection: value ? TextDirection.rtl : TextDirection.ltr,
          ),
        );
      },
377
      switchKey: const Key('text_direction'),
378 379 380 381 382 383 384
    );
  }
}

class _TimeDilationItem extends StatelessWidget {
  const _TimeDilationItem(this.options, this.onOptionsChanged);

385 386
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
387 388 389

  @override
  Widget build(BuildContext context) {
390
    return _BooleanItem(
391
      'Slow motion',
392
      options!.timeDilation != 1.0,
393
      (bool value) {
394 395
        onOptionsChanged!(
          options!.copyWith(
396 397 398 399
            timeDilation: value ? 20.0 : 1.0,
          ),
        );
      },
400
      switchKey: const Key('slow_motion'),
401 402 403 404 405 406 407
    );
  }
}

class _PlatformItem extends StatelessWidget {
  const _PlatformItem(this.options, this.onOptionsChanged);

408 409
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
410

411
  String? _platformLabel(TargetPlatform? platform) {
412 413 414 415 416 417 418
    switch(platform) {
      case TargetPlatform.android:
        return 'Mountain View';
      case TargetPlatform.fuchsia:
        return 'Fuchsia';
      case TargetPlatform.iOS:
        return 'Cupertino';
419 420
      case TargetPlatform.linux:
        return 'Material Desktop (linux)';
421 422
      case TargetPlatform.macOS:
        return 'Material Desktop (macOS)';
423 424
      case TargetPlatform.windows:
        return 'Material Desktop (Windows)';
425 426 427
      default:
        assert(false);
        return null;
428 429 430 431 432
    }
  }

  @override
  Widget build(BuildContext context) {
433 434
    return _OptionsItem(
      child: Row(
435
        children: <Widget>[
436 437
          Expanded(
            child: Column(
438 439 440
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text('Platform mechanics'),
441
                 Text(
442
                   _platformLabel(options!.platform)!,
443
                   style: Theme.of(context).primaryTextTheme.bodyText2,
444 445 446 447
                 ),
              ],
            ),
          ),
448
          PopupMenuButton<TargetPlatform>(
449 450 451 452
            padding: const EdgeInsetsDirectional.only(end: 16.0),
            icon: const Icon(Icons.arrow_drop_down),
            itemBuilder: (BuildContext context) {
              return TargetPlatform.values.map((TargetPlatform platform) {
453
                return PopupMenuItem<TargetPlatform>(
454
                  value: platform,
455
                  child: Text(_platformLabel(platform)!),
456 457 458 459
                );
              }).toList();
            },
            onSelected: (TargetPlatform platform) {
460 461
              onOptionsChanged!(
                options!.copyWith(platform: platform),
462 463 464 465 466 467 468 469 470 471 472
              );
            },
          ),
        ],
      ),
    );
  }
}

class GalleryOptionsPage extends StatelessWidget {
  const GalleryOptionsPage({
473
    Key? key,
474 475 476 477 478
    this.options,
    this.onOptionsChanged,
    this.onSendFeedback,
  }) : super(key: key);

479 480 481
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
  final VoidCallback? onSendFeedback;
482 483 484 485

  List<Widget> _enabledDiagnosticItems() {
    // Boolean showFoo options with a value of null: don't display
    // the showFoo option at all.
486
    if (options == null)
487 488
      return const <Widget>[];

489
    return <Widget>[
490 491
      const Divider(),
      const _Heading('Diagnostics'),
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
      _BooleanItem(
        'Highlight offscreen layers',
        options!.showOffscreenLayersCheckerboard,
        (bool value) {
          onOptionsChanged!(options!.copyWith(showOffscreenLayersCheckerboard: value));
        },
      ),
      _BooleanItem(
        'Highlight raster cache images',
        options!.showRasterCacheImagesCheckerboard,
        (bool value) {
          onOptionsChanged!(options!.copyWith(showRasterCacheImagesCheckerboard: value));
        },
      ),
      _BooleanItem(
        'Show performance overlay',
        options!.showPerformanceOverlay,
        (bool value) {
          onOptionsChanged!(options!.copyWith(showPerformanceOverlay: value));
        },
      ),
513
    ];
514 515 516 517 518 519
  }

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

520
    return DefaultTextStyle(
521
      style: theme.primaryTextTheme.subtitle1!,
522
      child: ListView(
523 524 525
        padding: const EdgeInsets.only(bottom: 124.0),
        children: <Widget>[
          const _Heading('Display'),
526
          _ThemeModeItem(options, onOptionsChanged),
527
          _TextScaleFactorItem(options, onOptionsChanged),
528
          _VisualDensityItem(options, onOptionsChanged),
529 530
          _TextDirectionItem(options, onOptionsChanged),
          _TimeDilationItem(options, onOptionsChanged),
531
          const Divider(),
532
          const ExcludeSemantics(child: _Heading('Platform mechanics')),
533
          _PlatformItem(options, onOptionsChanged),
534 535 536 537 538 539 540 541
          ..._enabledDiagnosticItems(),
          const Divider(),
          const _Heading('Flutter gallery'),
          _ActionItem('About Flutter Gallery', () {
            showGalleryAboutDialog(context);
          }),
          _ActionItem('Send feedback', onSendFeedback),
        ],
542 543 544 545
      ),
    );
  }
}