diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart index 49def3e353..a463f554bc 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -13,5 +13,6 @@ export 'src/foundation/assertions.dart'; export 'src/foundation/basic_types.dart'; export 'src/foundation/binding.dart'; export 'src/foundation/change_notifier.dart'; +export 'src/foundation/licenses.dart'; export 'src/foundation/print.dart'; export 'src/foundation/synchronous_future.dart'; diff --git a/packages/flutter/lib/src/foundation/binding.dart b/packages/flutter/lib/src/foundation/binding.dart index 5209dcdba6..76438569d7 100644 --- a/packages/flutter/lib/src/foundation/binding.dart +++ b/packages/flutter/lib/src/foundation/binding.dart @@ -11,6 +11,7 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; +import 'licenses.dart'; /// Signature for service extensions. /// @@ -74,9 +75,14 @@ abstract class BindingBase { /// `initInstances()`. void initInstances() { assert(!_debugInitialized); + LicenseRegistry.addLicense(_addLicenses); assert(() { _debugInitialized = true; return true; }); } + Iterable _addLicenses() sync* { + // TODO(ianh): Populate the license registry. + } + /// Called when the binding is initialized, to register service /// extensions. /// diff --git a/packages/flutter/lib/src/foundation/licenses.dart b/packages/flutter/lib/src/foundation/licenses.dart new file mode 100644 index 0000000000..356d7a773b --- /dev/null +++ b/packages/flutter/lib/src/foundation/licenses.dart @@ -0,0 +1,264 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Signature for callbacks passed to [LicenseRegistry.addLicense]. +typedef Iterable LicenseEntryCollector(); + +/// A string that represents one paragraph in a [LicenseEntry]. +/// +/// See [LicenseEntry.paragraphs]. +class LicenseParagraph { + /// Creates a string for a license entry paragraph. + const LicenseParagraph(this.text, this.indent); + + /// The text of the paragraph. Should not have any leading or trailing whitespace. + final String text; + + /// How many steps of indentation the paragraph has. + /// + /// * 0 means the paragraph is not indented. + /// * 1 means the paragraph is indented one unit of indentation. + /// * 2 means the paragraph is indented two units of indentation. + /// + /// ...and so forth. + /// + /// In addition, the special value [centeredIndent] can be used to indicate + /// that rather than being indented, the paragraph is centered. + final int indent; // can be set to centeredIndent + + /// A constant that represents "centered" alignment for [indent]. + static const int centeredIndent = -1; +} + +/// A license that covers part of the application's software or assets, to show +/// in an interface such as the [LicensePage]. +/// +/// For optimal performance, [LicenseEntry] objects should only be created on +/// demand in [LicenseEntryCollector] callbacks passed to +/// [LicenseRegistry.addLicense]. +abstract class LicenseEntry { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const LicenseEntry(); + + /// Returns each paragraph of the license as a [LicenseParagraph], which + /// consists of a string and some formatting information. Paragraphs can + /// include newline characters, but this is discouraged as it results in + /// ugliness. + Iterable get paragraphs; +} + +enum _LicenseEntryWithLineBreaksParserState { + beforeParagraph, inParagraph +} + +/// Variant of [LicenseEntry] for licenses that separate paragraphs with blank +/// lines and that hard-wrap text within paragraphs. Lines that begin with one +/// or more space characters are also assumed to introduce new paragraphs, +/// unless they start with the same number of spaces as the previous line, in +/// which case it's assumed they are a continuation of an indented paragraph. +/// +/// For example, the BSD license in this format could be encoded as follows: +/// +/// ```dart +/// LicenseRegistry.addLicense(() { +/// yield new LicenseEntryWithLineBreaks(''' +/// Copyright 2016 The Sample Authors. All rights reserved. +/// +/// Redistribution and use in source and binary forms, with or without +/// modification, are permitted provided that the following conditions are +/// met: +/// +/// * Redistributions of source code must retain the above copyright +/// notice, this list of conditions and the following disclaimer. +/// * Redistributions in binary form must reproduce the above +/// copyright notice, this list of conditions and the following disclaimer +/// in the documentation and/or other materials provided with the +/// distribution. +/// * Neither the name of Example Inc. nor the names of its +/// contributors may be used to endorse or promote products derived from +/// this software without specific prior written permission. +/// +/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +/// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +/// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +/// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +/// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +/// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +/// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +/// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +/// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +/// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +/// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''); +/// } +/// ``` +/// +/// This would result in a license with six [paragraphs], the third, fourth, and +/// fifth being indented one level. +class LicenseEntryWithLineBreaks extends LicenseEntry { + /// Create a license entry for a license whose text is hard-wrapped within + /// paragraphs and has paragraph breaks denoted by blank lines or with + /// indented text. + const LicenseEntryWithLineBreaks(this.text); + + /// The text of the license. + /// + /// The text will be split into paragraphs according to the following + /// conventions: + /// + /// * Lines starting with a different number of space characters than the + /// previous line start a new paragraph, with those spaces removed. + /// * Blank lines start a new paragraph. + /// * Other line breaks are replaced by a single space character. + /// * Leading spaces on a line are removed. + /// + /// For each paragraph, the algorithm attempts (using some rough heuristics) + /// to identify how indented the paragraph is, or whether it is centered. + final String text; + + @override + Iterable get paragraphs sync* { + int lineStart = 0; + int currentPosition = 0; + int lastLineIndent = 0; + int currentLineIndent = 0; + int currentParagraphIndentation; + _LicenseEntryWithLineBreaksParserState state = _LicenseEntryWithLineBreaksParserState.beforeParagraph; + List lines = []; + + void addLine() { + assert(lineStart < currentPosition); + lines.add(text.substring(lineStart, currentPosition)); + } + + LicenseParagraph getParagraph() { + assert(lines.isNotEmpty); + assert(currentParagraphIndentation != null); + final LicenseParagraph result = new LicenseParagraph(lines.join(' '), currentParagraphIndentation); + assert(result.text.trimLeft() == result.text); + assert(result.text.isNotEmpty); + lines.clear(); + return result; + } + + while (currentPosition < text.length) { + switch (state) { + case _LicenseEntryWithLineBreaksParserState.beforeParagraph: + assert(lineStart == currentPosition); + switch (text[currentPosition]) { + case ' ': + lineStart = currentPosition + 1; + currentLineIndent += 1; + state = _LicenseEntryWithLineBreaksParserState.beforeParagraph; + break; + case '\n': + case '\f': + if (lines.isNotEmpty) + yield getParagraph(); + lastLineIndent = 0; + currentLineIndent = 0; + currentParagraphIndentation = null; + lineStart = currentPosition + 1; + state = _LicenseEntryWithLineBreaksParserState.beforeParagraph; + break; + case '[': + // This is a bit of a hack for the LGPL 2.1, which does something like this: + // + // [this is a + // single paragraph] + // + // ...near the top. + currentLineIndent += 1; + continue startParagraph; + startParagraph: + default: + if (lines.isNotEmpty && currentLineIndent > lastLineIndent) { + yield getParagraph(); + currentParagraphIndentation = null; + } + // The following is a wild heuristic for guessing the indentation level. + // It happens to work for common variants of the BSD and LGPL licenses. + if (currentParagraphIndentation == null) { + if (currentLineIndent > 10) + currentParagraphIndentation = LicenseParagraph.centeredIndent; + else + currentParagraphIndentation = currentLineIndent ~/ 3; + } + state = _LicenseEntryWithLineBreaksParserState.inParagraph; + } + break; + case _LicenseEntryWithLineBreaksParserState.inParagraph: + switch (text[currentPosition]) { + case '\n': + addLine(); + lastLineIndent = currentLineIndent; + currentLineIndent = 0; + lineStart = currentPosition + 1; + state = _LicenseEntryWithLineBreaksParserState.beforeParagraph; + break; + case '\f': + addLine(); + yield getParagraph(); + lastLineIndent = 0; + currentLineIndent = 0; + currentParagraphIndentation = null; + lineStart = currentPosition + 1; + state = _LicenseEntryWithLineBreaksParserState.beforeParagraph; + break; + default: + state = _LicenseEntryWithLineBreaksParserState.inParagraph; + } + break; + } + currentPosition += 1; + } + switch (state) { + case _LicenseEntryWithLineBreaksParserState.beforeParagraph: + if (lines.isNotEmpty) + yield getParagraph(); + break; + case _LicenseEntryWithLineBreaksParserState.inParagraph: + addLine(); + yield getParagraph(); + break; + } + } +} + + +/// A registry for packages to add licenses to, so that they can be displayed +/// together in an interface such as the [LicensePage]. +/// +/// Packages should register their licenses using [addLicense]. User interfaces +/// that wish to show all the licenses can obtain them by calling [licenses]. +class LicenseRegistry { + LicenseRegistry._(); + + static List _collectors; + + /// Adds licenses to the registry. + /// + /// To avoid actually manipulating the licenses unless strictly necessary, + /// licenses are added by adding a closure that returns a list of + /// [LicenseEntry] objects. The closure is only called if [licenses] is itself + /// called; in normal operation, if the user does not request to see the + /// licenses, the closure will not be called. + static void addLicense(LicenseEntryCollector collector) { + _collectors ??= []; + _collectors.add(collector); + } + + /// Returns the licenses that have been registered. + /// + /// Each time the iterable returned by this function is called, the callbacks + /// registered with [addLicense] are called again, which is relatively + /// expensive. For this reason, consider immediately converting the results to + /// a list with [Iterable.toList]. + static Iterable get licenses sync* { + if (_collectors == null) + return; + for (LicenseEntryCollector collector in _collectors) + yield* collector(); + } +} \ No newline at end of file diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart index a4d3444b3a..1470736ed2 100644 --- a/packages/flutter/lib/src/material/about.dart +++ b/packages/flutter/lib/src/material/about.dart @@ -301,7 +301,7 @@ class AboutDialog extends StatelessWidget { /// /// To show a [LicensePage], use [showLicensePage]. // TODO(ianh): Mention the API for registering more licenses once it exists. -class LicensePage extends StatelessWidget { +class LicensePage extends StatefulWidget { /// Creates a page that shows licenses for software used by the application. /// /// The arguments are all optional. The application name, if omitted, will be @@ -337,27 +337,73 @@ class LicensePage extends StatelessWidget { /// Defaults to the empty string. final String applicationLegalese; + @override + _LicensePageState createState() => new _LicensePageState(); +} + +class _LicensePageState extends State { + List _licenses = _initLicenses(); + + static List _initLicenses() { + List result = []; + for (LicenseEntry license in LicenseRegistry.licenses) { + bool haveMargin = true; + result.add(new Padding( + padding: new EdgeInsets.symmetric(vertical: 18.0), + child: new Text( + '🍀‬', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere. + textAlign: TextAlign.center + ) + )); + for (LicenseParagraph paragraph in license.paragraphs) { + if (paragraph.indent == LicenseParagraph.centeredIndent) { + result.add(new Padding( + padding: new EdgeInsets.only(top: haveMargin ? 0.0 : 16.0), + child: new Text( + paragraph.text, + style: new TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center + ) + )); + } else { + assert(paragraph.indent >= 0); + result.add(new Padding( + padding: new EdgeInsets.only(top: haveMargin ? 0.0 : 8.0, left: 16.0 * paragraph.indent), + child: new Text(paragraph.text) + )); + } + haveMargin = false; + } + } + return result; + } + @override Widget build(BuildContext context) { - final String name = applicationName ?? _defaultApplicationName(context); - final String version = applicationVersion ?? _defaultApplicationVersion(context); + final String name = config.applicationName ?? _defaultApplicationName(context); + final String version = config.applicationVersion ?? _defaultApplicationVersion(context); + final List contents = [ + new Text(name, style: Theme.of(context).textTheme.headline, textAlign: TextAlign.center), + new Text(version, style: Theme.of(context).textTheme.body1, textAlign: TextAlign.center), + new Container(height: 18.0), + new Text(config.applicationLegalese ?? '', style: Theme.of(context).textTheme.caption, textAlign: TextAlign.center), + new Container(height: 18.0), + new Text('Powered by Flutter', style: Theme.of(context).textTheme.body1, textAlign: TextAlign.center), + new Container(height: 24.0), + ]; + contents.addAll(_licenses); return new Scaffold( appBar: new AppBar( title: new Text('Licenses') ), - body: new Block( - padding: new EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), - children: [ - new Text(name, style: Theme.of(context).textTheme.headline, textAlign: TextAlign.center), - new Text(version, style: Theme.of(context).textTheme.body1, textAlign: TextAlign.center), - new Container(height: 18.0), - new Text(applicationLegalese ?? '', style: Theme.of(context).textTheme.caption, textAlign: TextAlign.center), - new Container(height: 18.0), - new Text('Powered by Flutter', style: Theme.of(context).textTheme.body1, textAlign: TextAlign.center), - new Container(height: 24.0), - // TODO(ianh): Fill in the licenses from the API for registering more licenses once it exists. - new Text('', style: Theme.of(context).textTheme.caption) - ] + body: new DefaultTextStyle( + style: Theme.of(context).textTheme.caption, + child: new LazyBlock( + padding: new EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + delegate: new LazyBlockChildren( + children: contents + ) + ) ) ); } diff --git a/packages/flutter/test/foundation/licenses_test.dart b/packages/flutter/test/foundation/licenses_test.dart new file mode 100644 index 0000000000..112d12bdbe --- /dev/null +++ b/packages/flutter/test/foundation/licenses_test.dart @@ -0,0 +1,197 @@ +// Copyright 2016 The Chromium 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:test/test.dart'; + +void main() { + test('LicenseEntryWithLineBreaks - most cases', () { + // There's some trailing spaces in this string. + // To avoid IDEs stripping them, I've escaped them as \u0020. + List paragraphs = new LicenseEntryWithLineBreaks(''' +A +A +A + B +B +B + C +C +C + D +D +D + +E +E + F + G + G +G + +[H + H + H] +\u0020\u0020 +I J + K +K + +L +L L +L L +L L +L L +L L + + M +M\u0020\u0020\u0020 +M\u0020\u0020\u0020\u0020 + +N + +O +O + + +P + + + +QQQ + +RR RRR RRRR RRRRR +R + +S + + T + + U + V + + W + + X +\u0020\u0020\u0020\u0020\u0020\u0020 + Y''').paragraphs.toList(); + + int index = 0; + expect(paragraphs[index].text, 'A A A'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'B B B'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'C C C'); + expect(paragraphs[index].indent, 1); + index += 1; + expect(paragraphs[index].text, 'D D D'); + expect(paragraphs[index].indent, LicenseParagraph.centeredIndent); + index += 1; + expect(paragraphs[index].text, 'E E'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'F'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'G G G'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, '[H H H]'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'I'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'J'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'K K'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'L L L L L L L L L L L'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'M M M '); + expect(paragraphs[index].indent, 1); + index += 1; + expect(paragraphs[index].text, 'N'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'O O'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'P'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'QQQ'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'RR RRR RRRR RRRRR R'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'S'); + expect(paragraphs[index].indent, 0); + index += 1; + expect(paragraphs[index].text, 'T'); + expect(paragraphs[index].indent, 1); + index += 1; + expect(paragraphs[index].text, 'U'); + expect(paragraphs[index].indent, 2); + index += 1; + expect(paragraphs[index].text, 'V'); + expect(paragraphs[index].indent, 3); + index += 1; + expect(paragraphs[index].text, 'W'); + expect(paragraphs[index].indent, 2); + index += 1; + expect(paragraphs[index].text, 'X'); + expect(paragraphs[index].indent, 2); + index += 1; + expect(paragraphs[index].text, 'Y'); + expect(paragraphs[index].indent, 2); + index += 1; + expect(paragraphs, hasLength(index)); + }); + + test('LicenseEntryWithLineBreaks - leading and trailing whitespace', () { + expect(new LicenseEntryWithLineBreaks(' \n\n ').paragraphs.toList(), isEmpty); + + List paragraphs; + + paragraphs = new LicenseEntryWithLineBreaks(' \nA\n ').paragraphs.toList(); + expect(paragraphs[0].text, 'A'); + expect(paragraphs[0].indent, 0); + expect(paragraphs, hasLength(1)); + + paragraphs = new LicenseEntryWithLineBreaks('\n\n\nA\n\n\n').paragraphs.toList(); + expect(paragraphs[0].text, 'A'); + expect(paragraphs[0].indent, 0); + expect(paragraphs, hasLength(1)); + }); + + test('LicenseRegistry', () { + expect(LicenseRegistry.licenses, isEmpty); + LicenseRegistry.addLicense(() { + return [ + new LicenseEntryWithLineBreaks('A'), + new LicenseEntryWithLineBreaks('B'), + ]; + }); + LicenseRegistry.addLicense(() { + return [ + new LicenseEntryWithLineBreaks('C'), + new LicenseEntryWithLineBreaks('D'), + ]; + }); + expect(LicenseRegistry.licenses, hasLength(4)); + List licenses = LicenseRegistry.licenses.toList(); + expect(licenses, hasLength(4)); + expect(licenses[0].paragraphs.single.text, 'A'); + expect(licenses[1].paragraphs.single.text, 'B'); + expect(licenses[2].paragraphs.single.text, 'C'); + expect(licenses[3].paragraphs.single.text, 'D'); + }); +}