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

5
import 'package:flutter/cupertino.dart';
6
import 'package:flutter/material.dart';
7
import 'package:url_launcher/url_launcher.dart';
8

9
import 'demos.dart';
10
import 'example_code_parser.dart';
11
import 'syntax_highlighter.dart';
12

13
@immutable
14
class ComponentDemoTabData {
15
  const ComponentDemoTabData({
16
    this.demoWidget,
17
    this.exampleCodeTag,
18
    this.description,
19 20
    this.tabName,
    this.documentationUrl,
21 22
  });

23 24 25 26 27
  final Widget? demoWidget;
  final String? exampleCodeTag;
  final String? description;
  final String? tabName;
  final String? documentationUrl;
28 29 30

  @override
  bool operator==(Object other) {
31
    if (other.runtimeType != runtimeType) {
32
      return false;
33
    }
34 35 36 37
    return other is ComponentDemoTabData
        && other.tabName == tabName
        && other.description == description
        && other.documentationUrl == documentationUrl;
38 39 40
  }

  @override
41
  int get hashCode => Object.hash(tabName, description, documentationUrl);
42 43
}

44
class TabbedComponentDemoScaffold extends StatefulWidget {
45
  const TabbedComponentDemoScaffold({
46
    super.key,
47
    this.title,
48 49
    this.demos,
    this.actions,
50 51
    this.isScrollable = true,
    this.showExampleCodeAction = true,
52
  });
53

54 55 56
  final List<ComponentDemoTabData>? demos;
  final String? title;
  final List<Widget>? actions;
57 58
  final bool isScrollable;
  final bool showExampleCodeAction;
59

60 61 62 63 64
  @override
  State<TabbedComponentDemoScaffold> createState() => _TabbedComponentDemoScaffoldState();
}

class _TabbedComponentDemoScaffoldState extends State<TabbedComponentDemoScaffold> {
65
  void _showExampleCode(BuildContext context) {
66
    final String? tag = widget.demos![DefaultTabController.of(context).index].exampleCodeTag;
67
    if (tag != null) {
68 69
      Navigator.push(context, MaterialPageRoute<FullScreenCodeDialog>(
        builder: (BuildContext context) => FullScreenCodeDialog(exampleCodeTag: tag)
70 71 72 73
      ));
    }
  }

74
  Future<void> _showApiDocumentation(BuildContext context) async {
75
    final String? url = widget.demos![DefaultTabController.of(context).index].documentationUrl;
76
    if (url == null) {
77
      return;
78
    }
79

80 81 82
    final Uri uri = Uri.parse(url);
    if (await canLaunchUrl(uri)) {
      await launchUrl(uri);
83
    } else if (mounted) {
84 85 86 87
      showDialog<void>(
        context: context,
        builder: (BuildContext context) {
          return SimpleDialog(
88
            title: const Text("Couldn't display URL:"),
89 90 91 92 93 94 95 96 97
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16.0),
                child: Text(url),
              ),
            ],
          );
        },
      );
98 99 100
    }
  }

101 102
  @override
  Widget build(BuildContext context) {
103
    return DefaultTabController(
104
      length: widget.demos!.length,
105 106
      child: Scaffold(
        appBar: AppBar(
107
          title: Text(widget.title!),
108
          actions: <Widget>[
109
            ...?widget.actions,
110 111 112 113 114 115 116 117
            Builder(
              builder: (BuildContext context) {
                return IconButton(
                  icon: const Icon(Icons.library_books, semanticLabel: 'Show documentation'),
                  onPressed: () => _showApiDocumentation(context),
                );
              },
            ),
118
            if (widget.showExampleCodeAction)
119 120 121 122 123 124 125 126 127 128
              Builder(
                builder: (BuildContext context) {
                  return IconButton(
                    icon: const Icon(Icons.code),
                    tooltip: 'Show example code',
                    onPressed: () => _showExampleCode(context),
                  );
                },
              ),
          ],
129
          bottom: TabBar(
130 131
            isScrollable: widget.isScrollable,
            tabs: widget.demos!.map<Widget>((ComponentDemoTabData data) => Tab(text: data.tabName)).toList(),
Hans Muller's avatar
Hans Muller committed
132
          ),
133
        ),
134
        body: TabBarView(
135
          children: widget.demos!.map<Widget>((ComponentDemoTabData demo) {
136
            return SafeArea(
137 138
              top: false,
              bottom: false,
139
              child: Column(
140
                children: <Widget>[
141
                  Padding(
142
                    padding: const EdgeInsets.all(16.0),
143
                    child: Text(demo.description!,
144
                      style: Theme.of(context).textTheme.titleMedium,
145
                    ),
146
                  ),
147
                  Expanded(child: demo.demoWidget!),
148 149
                ],
              ),
150
            );
Hans Muller's avatar
Hans Muller committed
151 152 153
          }).toList(),
        ),
      ),
154 155 156 157
    );
  }
}

158
class FullScreenCodeDialog extends StatefulWidget {
159
  const FullScreenCodeDialog({ super.key, this.exampleCodeTag });
160

161
  final String? exampleCodeTag;
162 163

  @override
164
  FullScreenCodeDialogState createState() => FullScreenCodeDialogState();
165 166 167
}

class FullScreenCodeDialogState extends State<FullScreenCodeDialog> {
168

169
  String? _exampleCode;
170 171

  @override
172
  void didChangeDependencies() {
173
    getExampleCode(widget.exampleCodeTag, DefaultAssetBundle.of(context)).then((String? code) {
174 175
      if (mounted) {
        setState(() {
176
          _exampleCode = code ?? 'Example code not found';
177 178
        });
      }
179
    });
180
    super.didChangeDependencies();
181
  }
182 183 184

  @override
  Widget build(BuildContext context) {
185 186 187 188
    final SyntaxHighlighterStyle style = Theme.of(context).brightness == Brightness.dark
      ? SyntaxHighlighterStyle.darkThemeStyle()
      : SyntaxHighlighterStyle.lightThemeStyle();

189 190
    Widget body;
    if (_exampleCode == null) {
191
      body = const Center(
192
        child: CircularProgressIndicator(),
193 194
      );
    } else {
195 196
      body = SingleChildScrollView(
        child: Padding(
197
          padding: const EdgeInsets.all(16.0),
198 199
          child: RichText(
            text: TextSpan(
200
              style: const TextStyle(fontFamily: 'monospace', fontSize: 10.0),
201
              children: <TextSpan>[
202 203 204 205 206
                DartSyntaxHighlighter(style).format(_exampleCode),
              ],
            ),
          ),
        ),
207 208 209
      );
    }

210 211 212
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
213 214 215 216
          icon: const Icon(
            Icons.clear,
            semanticLabel: 'Close',
          ),
217
          onPressed: () { Navigator.pop(context); },
218
        ),
219
        title: const Text('Example code'),
220
      ),
221
      body: body,
222 223 224
    );
  }
}
225 226

class MaterialDemoDocumentationButton extends StatelessWidget {
227
  MaterialDemoDocumentationButton(String routeName, { super.key })
228 229 230 231
    : documentationUrl = kDemoDocumentationUrl[routeName],
      assert(
        kDemoDocumentationUrl[routeName] != null,
        'A documentation URL was not specified for demo route $routeName in kAllGalleryDemos',
232
      );
233

234
  final String? documentationUrl;
235 236 237 238 239

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: const Icon(Icons.library_books),
240
      tooltip: 'API documentation',
241
      onPressed: () => launchUrl(Uri.parse(documentationUrl!), mode: LaunchMode.inAppWebView),
242 243 244 245 246
    );
  }
}

class CupertinoDemoDocumentationButton extends StatelessWidget {
247
  CupertinoDemoDocumentationButton(String routeName, { super.key })
248 249 250 251
    : documentationUrl = kDemoDocumentationUrl[routeName],
      assert(
        kDemoDocumentationUrl[routeName] != null,
        'A documentation URL was not specified for demo route $routeName in kAllGalleryDemos',
252
      );
253

254
  final String? documentationUrl;
255 256 257 258 259

  @override
  Widget build(BuildContext context) {
    return CupertinoButton(
      padding: EdgeInsets.zero,
260 261 262 263
      child: Semantics(
        label: 'API documentation',
        child: const Icon(CupertinoIcons.book),
      ),
264
      onPressed: () => launchUrl(Uri.parse(documentationUrl!), mode: LaunchMode.inAppWebView),
265 266 267
    );
  }
}