commit 6d8b63618b8d1120d9dedd97fc02d7c5175ab64f Author: luckyrat Date: Wed Jul 7 10:14:27 2021 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb431f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..f5708fb --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 91f8d3da32cdbec4780bd9a449550f7c03465a47 + channel: master + +project_type: package diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..25f7376 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,144 @@ +## [1.0.3+2] +* Removed unnecessary asserts +* Removed null safety check on TreeView scroll physics property + +## [1.0.3+1] +* Null check bug fixes + +## [1.0.2+1] +* Bug fixes + +### Updated +* Fixes: Null check operator used on a null value +* Fixes: Node Data not being written when TreeViewController is converted back to a Map +Create issue + + +## [1.0.1+1] +* Updated internal references + +## [1.0.0+1] + +### Updated +* Updated project to null-safety + +## [0.10.0+1] + +### Updated +* Fixed NodeExpander Size issue +* Updated code to use proper hover color between web and mobile + +## [0.9.0+1] + +### Added +* Added expandSpeed property to TreeViewTheme to control the speed in which nodes are animated. +* Added node builder to TreeView to allow custom display of node data. Builder function accepts build context and Node as parameters. +* Added expandAll, collapseAll, withExpandAll and withCollapseAll to TreeViewController + +### Updated +* Refactored Node class to use IconData for the icon property. + +### Removed +* Removed NodeIcon class. + +## [0.8.0+1] + +### Added +* Added support for labelOverflow and parentLabelOverflow. Thanks to Long Ti. + +## [0.7.1+1] + +### Updated +* Refactored logic to prevent getter 'key' called on null error when calling expandToNode and collapseToNode functions. + +## [0.7.0+1] + +### Added +* Added support for vertical and horizontal spacing. Thanks to Long Ti. +* Added support for padding node icons. +* Added bool parent property to Node class to force node to act as parent. + +### Updated +* Updated expander theme to not default to black but instead use the color of the current theme. +* Removed background color from tree nodes that aren't selected + +## [0.6.0+1] + +### Added +* Added support for importing data property during JSON and Map load + +## [0.5.0+1] + +### Added +* Added support for using shrinkWrap, primary, and physics property on TreeView + +## [0.4.2+1] + +### Added +* Added support for using external font packages + +## [0.4.1+1] + +### Updated +* Updated TreeView widget so that it inherits the ThemeData from context + +## [0.4.0+1] + +### Added +* Added expandToNode method to TreeViewController to support expanding all nodes down to specified node. Returns List. +* Added collapseToNode method to TreeViewController to support collapsing all nodes down to specified node. Returns List. +* Added withExpandToNode method to TreeViewController to support expanding all nodes down to specified node. Returns TreeViewController. +* Added withCollapseToNode method to TreeViewController to support expanding all nodes down to specified node. Returns TreeViewController. + +## [0.3.0+1] + +### Added +* Added generic data property to Node class to support the use of custom data + +## [0.2.0+1] + +### Updated +* Added animation controller dispose to TreeNode to prevent memory leaks + +### Added +* Added new dense property to TreeViewTheme +* Added new loadJSON and loadMap convenience methods to TreeViewController for data loading +* Added new convenience methods to TreeViewController: toggleNode, withToggleNode, selectedNode + +## [0.1.0+2] + +### Updated +* Updated links to repository documentation +* Cleaned up warnings + +## [0.0.4+1] + +### Updated +* Added logic to update TreeNode when expanded programmatically +* Fixed issue with adding new node to a TreeNode with new children + +## [0.0.3+7] + +### Added +* Added api documentation + +### Updated +* Added parentLabelStyle to TreeViewTheme to support separate styling for parent node + +## [0.0.2+1] + +### Added +* Added ExpanderModifier + +### Updated +* Updated open source license +* Simplified ExpanderType +* Refactored TreeNodeExpander class and added animation to icon +* Updated default expander size + +### Removed +* Removed custom TreeView font + +## [0.0.1] + +* Initial package release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8b3b158 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Romain Rastel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f98a4d --- /dev/null +++ b/README.md @@ -0,0 +1,172 @@ +# flutter_treeview + +A hierarchical data widget for your flutter apps. + +It offers a number of options for customizing the appearance and handling user interaction. + +It also offers some convenience methods for importing data into the tree. + + +## Features + +* Separately customize child and parent labels +* Add any icon to a node +* Choose from four different expander icons and several modifiers for adjusting shape, outline, and fill. +* Import data from a Map +* Includes ability to handle expandChange, tap, and double tap user interactions +* Includes convenience methods for adding, updating and deleting nodes + + +## Sample Code +### Creating a TreeView +```dart +List nodes = [ + Node( + label: 'Documents', + key: 'docs', + expanded: true, + icon: docsOpen ? Icons.folder_open : Icons.folder, + children: [ + Node( + label: 'Job Search', + key: 'd3', + icon: Icons.input, + children: [ + Node( + label: 'Resume.docx', + key: 'pd1', + icon: Icons.insert_drive_file, + ), + Node( + label: 'Cover Letter.docx', + key: 'pd2', + icon: Icons.insert_drive_file), + ]), + Node( + label: 'Inspection.docx', + key: 'd1', + ), + Node( + label: 'Invoice.docx', + key: 'd2', + icon: Icons.insert_drive_file), + ], + ), + Node( + label: 'MeetingReport.xls', + key: 'mrxls', + icon: Icons.insert_drive_file), + Node( + label: 'MeetingReport.pdf', + key: 'mrpdf', + icon: Icons.insert_drive_file), + Node( + label: 'Demo.zip', + key: 'demo', + icon: Icons.archive), +]; +TreeViewController _treeViewController = TreeViewController(children: nodes); +TreeView( + controller: _treeViewController, + allowParentSelect: false, + supportParentDoubleTap: false, + onExpansionChanged: _expandNodeHandler, + onNodeTap: (key) { + setState(() { + _treeViewController = _treeViewController.copyWith(selectedKey: key); + }); + }, + theme: treeViewTheme +), +``` +_The TreeView requires that the onExpansionChange property updates the expanded +node so that the tree is rendered properly. + +### Creating a theme +```dart +TreeViewTheme _treeViewTheme = TreeViewTheme( + expanderTheme: ExpanderThemeData( + type: ExpanderType.caret, + modifier: ExpanderModifier.none, + position: ExpanderPosition.start, + color: Colors.red.shade800, + size: 20, + ), + labelStyle: TextStyle( + fontSize: 16, + letterSpacing: 0.3, + ), + parentLabelStyle: TextStyle( + fontSize: 16, + letterSpacing: 0.1, + fontWeight: FontWeight.w800, + color: Colors.red.shade600, + ), + iconTheme: IconThemeData( + size: 18, + color: Colors.grey.shade800, + ), + colorScheme: ColorScheme.light(), +) +``` + +### Using custom data +The Node class supports the use of custom data. You can use the data property on the Node instance to store data that you want to easily retrieve. +```dart +class Person { + final String name; + final List pets; + + Person({this.name, this.pets}); +} + +class Animal { + final String name; + + Animal({this.name}); +} + +Animal otis = Animal(name: 'Otis'); +Animal zorro = Animal(name: 'Zorro'); +Person lukas = Person(name: 'Lukas', pets: [otis, zorro]); + +List nodes = [ + Node( + label: 'Lukas', + key: 'lukas', + data: lukas, + children: [ + Node( + label: 'Otis', + key: 'otis', + data: otis, + ), + // is optional but recommended. If not specified, code can return Node instead of Node + Node( + label: 'Zorro', + key: 'zorro', + data: zorro, + ), + ] + ), +]; +TreeViewController _treeViewController = TreeViewController(children: nodes); +TreeView( + controller: _treeViewController, + onNodeTap: (key) { + Node selectedNode = _treeViewController.getNode(key); + Person selectedModel = selectedNode.data; + }, +), +``` + + +## Getting Started + +For help getting started with this widget, view our +[online documentation](https://bitbucket.org/kevinandre/flutter_treeview/wiki/Home) or view the +full [API reference](https://pub.dev/documentation/flutter_treeview/latest/). + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/builddocs.sh b/builddocs.sh new file mode 100755 index 0000000..ba73708 --- /dev/null +++ b/builddocs.sh @@ -0,0 +1 @@ +dartdoc --include flutter_treeview ./ \ No newline at end of file diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..c320f9a --- /dev/null +++ b/example/README.md @@ -0,0 +1,12 @@ +# Example + +## Example Project +View example code in the [TreeView Test Example](https://bitbucket.org/kevinandre/flutter_treeview_example/src/master/) + +## Example Code +There are also some examples in the [unit tests](https://bitbucket.org/kevinandre/flutter_treeview/src/master/test/). + +## Screenshots +![Overview](https://bitbucket.org/kevinandre/flutter_treeview/raw/03707dec7b46e18b6d7a68867697bb7251906b75/screenshots/ss1.gif) + +![Overview](https://bitbucket.org/kevinandre/flutter_treeview/raw/03707dec7b46e18b6d7a68867697bb7251906b75/screenshots/ss2.png) \ No newline at end of file diff --git a/lib/flutter_treeview.dart b/lib/flutter_treeview.dart new file mode 100644 index 0000000..b71f772 --- /dev/null +++ b/lib/flutter_treeview.dart @@ -0,0 +1,9 @@ +library flutter_treeview; + +export 'src/expander_theme_data.dart'; +export 'src/models/node.dart'; +export 'src/tree_node.dart'; +export 'src/tree_view.dart'; +export 'src/tree_view_controller.dart'; +export 'src/tree_view_theme.dart'; +export 'src/utilities.dart'; diff --git a/lib/src/expander_theme_data.dart b/lib/src/expander_theme_data.dart new file mode 100644 index 0000000..50e98dd --- /dev/null +++ b/lib/src/expander_theme_data.dart @@ -0,0 +1,133 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_treeview/src/tree_view_theme.dart'; + +const double _kDefaultExpanderSize = 30.0; + +/// Defines whether expander icon is shown on the +/// left or right side of the parent node label. +enum ExpanderPosition { + start, + end, +} + +/// Defines the type expander icon displayed. All +/// types except the plus-minus type will be animated +enum ExpanderType { + caret, + arrow, + chevron, + plusMinus, +} + +/// Defines whether expander icon has a circle or square shape +/// and whether it is outlined or filled. +enum ExpanderModifier { + none, + circleFilled, + circleOutlined, + squareFilled, + squareOutlined, +} + +/// Defines the appearance of the expander icons. +/// +/// Used by [TreeViewTheme] to control the appearance of the expander icons for a +/// parent tree node in the [TreeView] widget. +class ExpanderThemeData { + /// The [ExpanderPosition] for expander icon. + final ExpanderPosition position; + + /// The [ExpanderType] for expander icon. + final ExpanderType type; + + /// The size for expander icon. + final double size; + + /// The color for expander icon. + final Color? color; + + /// The [ExpanderModifier] for expander icon. + final ExpanderModifier modifier; + + /// The animation state for expander icon. It determines whether + /// the icon animates when changing states + final bool animated; + + const ExpanderThemeData({ + this.color, + this.position: ExpanderPosition.start, + this.type: ExpanderType.caret, + this.size: _kDefaultExpanderSize, + this.modifier: ExpanderModifier.none, + this.animated: true, + }); + + /// Creates an expander icon theme with some reasonable default values. + /// + /// The [color] is black, + /// the [position] is [ExpanderPosition.start], + /// the [type] is [ExpanderType.caret], + /// the [modifier] is [ExpanderModifier.none], + /// the [animated] property is true, + /// and the [size] is 30.0. + const ExpanderThemeData.fallback() + : color = const Color(0xFF000000), + position = ExpanderPosition.start, + type = ExpanderType.caret, + modifier = ExpanderModifier.none, + animated = true, + size = _kDefaultExpanderSize; + + /// Creates a copy of this theme but with the given fields replaced with + /// the new values. + ExpanderThemeData copyWith({ + Color? color, + ExpanderType? type, + ExpanderPosition? position, + ExpanderModifier? modifier, + bool? animated, + double? size, + }) { + return ExpanderThemeData( + color: color ?? this.color, + type: type ?? this.type, + position: position ?? this.position, + modifier: modifier ?? this.modifier, + size: size ?? this.size, + animated: animated ?? this.animated, + ); + } + + /// Returns a new theme that matches this expander theme but with some values + /// replaced by the non-null parameters of the given icon theme. If the given + /// expander theme is null, simply returns this theme. + ExpanderThemeData merge(ExpanderThemeData? other) { + if (other == null) return this; + return copyWith( + color: other.color, + type: other.type, + position: other.position, + modifier: other.modifier, + animated: other.animated, + size: other.size, + ); + } + + ExpanderThemeData resolve(BuildContext context) => this; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is ExpanderThemeData && + other.color == color && + other.position == position && + other.type == type && + other.modifier == modifier && + other.animated == animated && + other.size == size; + } + + @override + int get hashCode => + hashValues(color, position, type, size, modifier, animated); +} diff --git a/lib/src/models/node.dart b/lib/src/models/node.dart new file mode 100644 index 0000000..16ac91a --- /dev/null +++ b/lib/src/models/node.dart @@ -0,0 +1,179 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:flutter/widgets.dart'; + +import '../tree_node.dart'; +import '../utilities.dart'; + +/// Defines the data used to display a [TreeNode]. +/// +/// Used by [TreeView] to display a [TreeNode]. +/// +/// This object allows the creation of key, label and icon to display +/// a node on the [TreeView] widget. The key and label properties are +/// required. The key is needed for events that occur on the generated +/// [TreeNode]. It should always be unique. +class Node { + /// The unique string that identifies this object. + final String key; + + /// The string value that is displayed on the [TreeNode]. + final String label; + + /// An optional icon that is displayed on the [TreeNode]. + final IconData? icon; + + /// The open or closed state of the [TreeNode]. Applicable only if the + /// node is a parent + final bool expanded; + + /// Generic data model that can be assigned to the [TreeNode]. This makes + /// it useful to assign and retrieve data associated with the [TreeNode] + final T? data; + + /// The sub [Node]s of this object. + final List children; + + /// Force the node to be a parent so that node can show expander without + /// having children node. + final bool parent; + + const Node({ + required this.key, + required this.label, + this.children: const [], + this.expanded: false, + this.parent: false, + this.icon, + this.data, + }); + + /// Creates a [Node] from a string value. It generates a unique key. + factory Node.fromLabel(String label) { + String _key = Utilities.generateRandom(); + return Node( + key: '${_key}_$label', + label: label, + ); + } + + /// Creates a [Node] from a Map map. The map + /// should contain a "label" value. If the key value is + /// missing, it generates a unique key. + /// If the expanded value, if present, can be any 'truthful' + /// value. Excepted values include: 1, yes, true and their + /// associated string values. + factory Node.fromMap(Map map) { + String? _key = map['key']; + String _label = map['label']; + var _data = map['data']; + List _children = []; + if (_key == null) { + _key = Utilities.generateRandom(); + } + // if (map['icon'] != null) { + // int _iconData = int.parse(map['icon']); + // if (map['icon'].runtimeType == String) { + // _iconData = int.parse(map['icon']); + // } else if (map['icon'].runtimeType == double) { + // _iconData = (map['icon'] as double).toInt(); + // } else { + // _iconData = map['icon']; + // } + // _icon = const IconData(_iconData); + // } + if (map['children'] != null) { + List> _childrenMap = List.from(map['children']); + _children = _childrenMap + .map((Map child) => Node.fromMap(child)) + .toList(); + } + return Node( + key: '$_key', + label: _label, + data: _data, + expanded: Utilities.truthful(map['expanded']), + parent: Utilities.truthful(map['parent']), + children: _children, + ); + } + + /// Creates a copy of this object but with the given fields + /// replaced with the new values. + Node copyWith({ + String? key, + String? label, + List? children, + bool? expanded, + bool? parent, + IconData? icon, + T? data, + }) => + Node( + key: key ?? this.key, + label: label ?? this.label, + icon: icon ?? this.icon, + expanded: expanded ?? this.expanded, + parent: parent ?? this.parent, + children: children ?? this.children, + data: data ?? this.data, + ); + + /// Whether this object has children [Node]. + bool get isParent => children.isNotEmpty || parent; + + /// Whether this object has a non-null icon. + bool get hasIcon => icon != null && icon != null; + + /// Whether this object has data associated with it. + bool get hasData => data != null; + + /// Map representation of this object + Map get asMap { + Map _map = { + "key": key, + "label": label, + "icon": icon == null ? null : icon!.codePoint, + "expanded": expanded, + "parent": parent, + "children": children.map((Node child) => child.asMap).toList(), + }; + if (data != null) { + _map['data'] = data; + } + //TODO: figure out a means to check for getter or method on generic to include map from generic + return _map; + } + + @override + String toString() { + return JsonEncoder().convert(asMap); + } + + @override + int get hashCode { + return hashValues( + key, + label, + icon, + expanded, + parent, + children, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is Node && + other.key == key && + other.label == label && + other.icon == icon && + other.expanded == expanded && + other.parent == parent && + other.data.runtimeType == T && + other.children.length == children.length; + } +} diff --git a/lib/src/tree_node.dart b/lib/src/tree_node.dart new file mode 100644 index 0000000..2e4ab6a --- /dev/null +++ b/lib/src/tree_node.dart @@ -0,0 +1,479 @@ +import 'dart:math' show pi; + +import 'package:flutter/material.dart'; + +import 'tree_view.dart'; +import 'tree_view_theme.dart'; +import 'expander_theme_data.dart'; +import 'models/node.dart'; + +const double _kBorderWidth = 0.75; + +/// Defines the [TreeNode] widget. +/// +/// This widget is used to display a tree node and its children. It requires +/// a single [Node] value. It uses this node to display the state of the +/// widget. It uses the [TreeViewTheme] to handle the appearance and the +/// [TreeView] properties to handle to user actions. +/// +/// __This class should not be used directly!__ +/// The [TreeView] and [TreeViewController] handlers the data and rendering +/// of the nodes. +class TreeNode extends StatefulWidget { + /// The node object used to display the widget state + final Node node; + + const TreeNode({Key? key, required this.node}) : super(key: key); + + @override + _TreeNodeState createState() => _TreeNodeState(); +} + +class _TreeNodeState extends State + with SingleTickerProviderStateMixin { + static final Animatable _easeInTween = + CurveTween(curve: Curves.easeIn); + + late AnimationController _controller; + late Animation _heightFactor; + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + _controller = + AnimationController(duration: Duration(milliseconds: 200), vsync: this); + _heightFactor = _controller.drive(_easeInTween); + _isExpanded = widget.node.expanded; + if (_isExpanded) _controller.value = 1.0; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + TreeView? _treeView = TreeView.of(context); + _controller.duration = _treeView!.theme.expandSpeed; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(TreeNode oldWidget) { + if (widget.node.expanded != oldWidget.node.expanded) { + setState(() { + _isExpanded = widget.node.expanded; + if (_isExpanded) { + _controller.forward(); + } else { + _controller.reverse().then((void value) { + if (!mounted) return; + setState(() {}); + }); + } + }); + } else if (widget.node != oldWidget.node) { + setState(() {}); + } + super.didUpdateWidget(oldWidget); + } + + void _handleExpand() { + TreeView? _treeView = TreeView.of(context); + assert(_treeView != null, 'TreeView must exist in context'); + setState(() { + _isExpanded = !_isExpanded; + if (_isExpanded) { + _controller.forward(); + } else { + _controller.reverse().then((void value) { + if (!mounted) return; + setState(() {}); + }); + } + }); + if (_treeView!.onExpansionChanged != null) + _treeView.onExpansionChanged!(widget.node.key, _isExpanded); + } + + void _handleTap() { + TreeView? _treeView = TreeView.of(context); + assert(_treeView != null, 'TreeView must exist in context'); + if (_treeView!.onNodeTap != null) { + _treeView.onNodeTap!(widget.node.key); + } + } + + void _handleDoubleTap() { + TreeView? _treeView = TreeView.of(context); + assert(_treeView != null, 'TreeView must exist in context'); + if (_treeView!.onNodeDoubleTap != null) { + _treeView.onNodeDoubleTap!(widget.node.key); + } + } + + Widget _buildNodeExpander() { + TreeView? _treeView = TreeView.of(context); + assert(_treeView != null, 'TreeView must exist in context'); + TreeViewTheme _theme = _treeView!.theme; + return widget.node.isParent + ? GestureDetector( + onTap: () => _handleExpand(), + child: _TreeNodeExpander( + speed: _controller.duration!, + expanded: widget.node.expanded, + themeData: _theme.expanderTheme, + ), + ) + : Container(width: _theme.expanderTheme.size); + } + + Widget _buildNodeIcon() { + TreeView? _treeView = TreeView.of(context); + assert(_treeView != null, 'TreeView must exist in context'); + TreeViewTheme _theme = _treeView!.theme; + bool isSelected = _treeView.controller.selectedKey != null && + _treeView.controller.selectedKey == widget.node.key; + return Container( + alignment: Alignment.center, + width: + widget.node.hasIcon ? _theme.iconTheme.size! + _theme.iconPadding : 0, + child: widget.node.hasIcon + ? Icon( + widget.node.icon, + size: _theme.iconTheme.size, + color: isSelected + ? _theme.colorScheme.onPrimary + : _theme.iconTheme.color, + ) + : null, + ); + } + + Widget _buildNodeLabel() { + TreeView? _treeView = TreeView.of(context); + assert(_treeView != null, 'TreeView must exist in context'); + TreeViewTheme _theme = _treeView!.theme; + bool isSelected = _treeView.controller.selectedKey != null && + _treeView.controller.selectedKey == widget.node.key; + final icon = _buildNodeIcon(); + return Container( + padding: EdgeInsets.symmetric( + vertical: _theme.verticalSpacing ?? (_theme.dense ? 10 : 15), + horizontal: 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + icon, + Expanded( + child: Text( + widget.node.label, + softWrap: widget.node.isParent + ? _theme.parentLabelOverflow == null + : _theme.labelOverflow == null, + overflow: widget.node.isParent + ? _theme.parentLabelOverflow + : _theme.labelOverflow, + style: widget.node.isParent + ? _theme.parentLabelStyle.copyWith( + fontWeight: _theme.parentLabelStyle.fontWeight, + color: isSelected + ? _theme.colorScheme.onPrimary + : _theme.parentLabelStyle.color, + ) + : _theme.labelStyle.copyWith( + fontWeight: _theme.labelStyle.fontWeight, + color: isSelected ? _theme.colorScheme.onPrimary : null, + ), + ), + ), + ], + ), + ); + } + + Widget _buildNodeWidget() { + TreeView? _treeView = TreeView.of(context); + assert(_treeView != null, 'TreeView must exist in context'); + TreeViewTheme _theme = _treeView!.theme; + bool isSelected = _treeView.controller.selectedKey != null && + _treeView.controller.selectedKey == widget.node.key; + bool canSelectParent = _treeView.allowParentSelect; + final arrowContainer = _buildNodeExpander(); + final labelContainer = _treeView.nodeBuilder != null + ? _treeView.nodeBuilder!(context, widget.node) + : _buildNodeLabel(); + Widget _tappable = _treeView.onNodeDoubleTap != null + ? InkWell( + onTap: _handleTap, + onDoubleTap: _handleDoubleTap, + child: labelContainer, + ) + : InkWell( + onTap: _handleTap, + child: labelContainer, + ); + if (widget.node.isParent) { + if (_treeView.supportParentDoubleTap && canSelectParent) { + _tappable = InkWell( + onTap: canSelectParent ? _handleTap : _handleExpand, + onDoubleTap: () { + _handleExpand(); + _handleDoubleTap(); + }, + child: labelContainer, + ); + } else if (_treeView.supportParentDoubleTap) { + _tappable = InkWell( + onTap: _handleExpand, + onDoubleTap: _handleDoubleTap, + child: labelContainer, + ); + } else { + _tappable = InkWell( + onTap: canSelectParent ? _handleTap : _handleExpand, + child: labelContainer, + ); + } + } + return Container( + color: isSelected ? _theme.colorScheme.primary : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: _theme.expanderTheme.position == ExpanderPosition.end + ? [ + Expanded( + child: _tappable, + ), + arrowContainer, + ] + : [ + arrowContainer, + Expanded( + child: _tappable, + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + TreeView? _treeView = TreeView.of(context); + assert(_treeView != null, 'TreeView must exist in context'); + final bool closed = + (!_isExpanded || !widget.node.expanded) && _controller.isDismissed; + final nodeWidget = _buildNodeWidget(); + return widget.node.isParent + ? AnimatedBuilder( + animation: _controller.view, + builder: (BuildContext context, Widget? child) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + nodeWidget, + ClipRect( + child: Align( + heightFactor: _heightFactor.value, + child: child, + ), + ), + ], + ); + }, + child: closed + ? null + : Container( + margin: EdgeInsets.only( + left: _treeView!.theme.horizontalSpacing ?? + _treeView.theme.iconTheme.size!), + child: Column( + mainAxisSize: MainAxisSize.min, + children: widget.node.children.map((Node node) { + return TreeNode(node: node); + }).toList()), + ), + ) + : Container( + child: nodeWidget, + ); + } +} + +class _TreeNodeExpander extends StatefulWidget { + final ExpanderThemeData themeData; + final bool expanded; + final Duration _expandSpeed; + + const _TreeNodeExpander({ + required Duration speed, + required this.themeData, + required this.expanded, + }) : _expandSpeed = speed; + + @override + _TreeNodeExpanderState createState() => _TreeNodeExpanderState(); +} + +class _TreeNodeExpanderState extends State<_TreeNodeExpander> + with SingleTickerProviderStateMixin { + late Animation animation; + late AnimationController controller; + + @override + void initState() { + bool isEnd = widget.themeData.position == ExpanderPosition.end; + if (widget.themeData.type != ExpanderType.plusMinus) { + controller = AnimationController( + duration: widget.themeData.animated + ? isEnd + ? widget._expandSpeed * 0.625 + : widget._expandSpeed + : Duration(milliseconds: 0), + vsync: this, + ); + animation = Tween( + begin: 0, + end: isEnd ? 180 : 90, + ).animate(controller); + } else { + controller = + AnimationController(duration: Duration(milliseconds: 0), vsync: this); + animation = Tween(begin: 0, end: 0).animate(controller); + } + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(_TreeNodeExpander oldWidget) { + if (widget.themeData != oldWidget.themeData || + widget.expanded != oldWidget.expanded) { + bool isEnd = widget.themeData.position == ExpanderPosition.end; + setState(() { + if (widget.themeData.type != ExpanderType.plusMinus) { + controller.duration = widget.themeData.animated + ? isEnd + ? widget._expandSpeed * 0.625 + : widget._expandSpeed + : Duration(milliseconds: 0); + animation = Tween( + begin: 0, + end: isEnd ? 180 : 90, + ).animate(controller); + } else { + controller.duration = Duration(milliseconds: 0); + animation = Tween(begin: 0, end: 0).animate(controller); + } + }); + } + super.didUpdateWidget(oldWidget); + } + + Color? _onColor(Color? color) { + if (color != null) { + if (color.computeLuminance() > 0.6) { + return Colors.black; + } else { + return Colors.white; + } + } + return null; + } + + @override + Widget build(BuildContext context) { + IconData _arrow; + double _iconSize = widget.themeData.size; + double _borderWidth = 0; + BoxShape _shapeBorder = BoxShape.rectangle; + Color _backColor = Colors.transparent; + Color? _iconColor = + widget.themeData.color ?? Theme.of(context).iconTheme.color; + switch (widget.themeData.modifier) { + case ExpanderModifier.none: + break; + case ExpanderModifier.circleFilled: + _shapeBorder = BoxShape.circle; + _backColor = widget.themeData.color ?? Colors.black; + _iconColor = _onColor(_backColor); + break; + case ExpanderModifier.circleOutlined: + _borderWidth = _kBorderWidth; + _shapeBorder = BoxShape.circle; + break; + case ExpanderModifier.squareFilled: + _backColor = widget.themeData.color ?? Colors.black; + _iconColor = _onColor(_backColor); + break; + case ExpanderModifier.squareOutlined: + _borderWidth = _kBorderWidth; + break; + } + switch (widget.themeData.type) { + case ExpanderType.chevron: + _arrow = Icons.expand_more; + break; + case ExpanderType.arrow: + _arrow = Icons.arrow_downward; + _iconSize = widget.themeData.size > 20 + ? widget.themeData.size - 8 + : widget.themeData.size; + break; + case ExpanderType.caret: + _arrow = Icons.arrow_drop_down; + break; + case ExpanderType.plusMinus: + _arrow = widget.expanded ? Icons.remove : Icons.add; + break; + } + + Icon _icon = Icon( + _arrow, + size: _iconSize, + color: _iconColor, + ); + + if (widget.expanded) { + controller.reverse(); + } else { + controller.forward(); + } + return Container( + width: widget.themeData.size + 2, + height: widget.themeData.size + 2, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: _shapeBorder, + border: _borderWidth == 0 + ? null + : Border.all( + width: _borderWidth, + color: widget.themeData.color ?? Colors.black, + ), + color: _backColor, + ), + child: AnimatedBuilder( + animation: controller, + child: _icon, + builder: (context, child) { + return Transform.rotate( + angle: animation.value * (-pi / 180), + child: child, + ); + }, + ), + ); + } +} diff --git a/lib/src/tree_view.dart b/lib/src/tree_view.dart new file mode 100644 index 0000000..cfa9d90 --- /dev/null +++ b/lib/src/tree_view.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; + +import 'tree_view_controller.dart'; +import 'tree_view_theme.dart'; +import 'tree_node.dart'; +import 'models/node.dart'; + +/// Defines the [TreeView] widget. +/// +/// This is the main widget for the package. It requires a controller +/// and allows you to specify other optional properties that manages +/// the appearance and handle events. +/// +/// ```dart +/// TreeView( +/// controller: _treeViewController, +/// allowParentSelect: false, +/// supportParentDoubleTap: false, +/// onExpansionChanged: _expandNodeHandler, +/// onNodeTap: (key) { +/// setState(() { +/// _treeViewController = _treeViewController.copyWith(selectedKey: key); +/// }); +/// }, +/// theme: treeViewTheme +/// ), +/// ``` +class TreeView extends InheritedWidget { + /// The controller for the [TreeView]. It manages the data and selected key. + final TreeViewController controller; + + /// The tap handler for a node. Passes the node key. + final Function(String)? onNodeTap; + + /// Custom builder for nodes. Parameters are the build context and tree node. + final Widget Function(BuildContext, Node)? nodeBuilder; + + /// The double tap handler for a node. Passes the node key. + final Function(String)? onNodeDoubleTap; + + /// The expand/collapse handler for a node. Passes the node key and the + /// expansion state. + final Function(String, bool)? onExpansionChanged; + + /// The theme for [TreeView]. + final TreeViewTheme theme; + + /// Determines whether the user can select a parent node. If false, + /// tapping the parent will expand or collapse the node. If true, the node + /// will be selected and the use has to use the expander to expand or + /// collapse the node. + final bool allowParentSelect; + + /// How the [TreeView] should respond to user input. + final ScrollPhysics? physics; + + /// Whether the extent of the [TreeView] should be determined by the contents + /// being viewed. + /// + /// Defaults to false. + final bool shrinkWrap; + + /// Whether the [TreeView] is the primary scroll widget associated with the + /// parent PrimaryScrollController.. + /// + /// Defaults to true. + final bool primary; + + /// Determines whether the parent node can receive a double tap. This is + /// useful if [allowParentSelect] is true. This allows the user to double tap + /// the parent node to expand or collapse the parent when [allowParentSelect] + /// is true. + /// ___IMPORTANT___ + /// _When true, the tap handler is delayed. This is because the double tap + /// action requires a short delay to determine whether the user is attempting + /// a single or double tap._ + final bool supportParentDoubleTap; + + TreeView({ + Key? key, + required this.controller, + this.onNodeTap, + this.onNodeDoubleTap, + this.physics, + this.onExpansionChanged, + this.allowParentSelect: false, + this.supportParentDoubleTap: false, + this.shrinkWrap: false, + this.primary: true, + this.nodeBuilder, + TreeViewTheme? theme, + }) : this.theme = theme ?? const TreeViewTheme(), + super( + key: key, + child: _TreeViewData( + controller, + shrinkWrap: shrinkWrap, + primary: primary, + physics: physics, + ), + ); + + static TreeView? of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType(aspect: TreeView); + + @override + bool updateShouldNotify(TreeView oldWidget) { + return oldWidget.controller.children != this.controller.children || + oldWidget.onNodeTap != this.onNodeTap || + oldWidget.onExpansionChanged != this.onExpansionChanged || + oldWidget.theme != this.theme || + oldWidget.supportParentDoubleTap != this.supportParentDoubleTap || + oldWidget.allowParentSelect != this.allowParentSelect; + } +} + +class _TreeViewData extends StatelessWidget { + final TreeViewController _controller; + final bool? shrinkWrap; + final bool? primary; + final ScrollPhysics? physics; + + const _TreeViewData(this._controller, + {this.shrinkWrap, this.primary, this.physics}); + + @override + Widget build(BuildContext context) { + ThemeData _parentTheme = Theme.of(context); + return Theme( + data: _parentTheme.copyWith(hoverColor: Colors.grey.shade100), + child: ListView( + shrinkWrap: shrinkWrap!, + primary: primary, + physics: physics, + padding: EdgeInsets.zero, + children: _controller.children.map((Node node) { + return TreeNode(node: node); + }).toList(), + ), + ); + } +} diff --git a/lib/src/tree_view_controller.dart b/lib/src/tree_view_controller.dart new file mode 100644 index 0000000..6f1fa9c --- /dev/null +++ b/lib/src/tree_view_controller.dart @@ -0,0 +1,487 @@ +import 'dart:convert' show jsonDecode, jsonEncode; + +import 'models/node.dart'; + +/// Defines the insertion mode adding a new [Node] to the [TreeView]. +enum InsertMode { + prepend, + append, + insert, +} + +/// Defines the controller needed to display the [TreeView]. +/// +/// Used by [TreeView] to display the nodes and selected node. +/// +/// This class also defines methods used to manipulate data in +/// the [TreeView]. The methods ([addNode], [updateNode], +/// and [deleteNode]) are non-mutilating, meaning they will not +/// modify the tree but instead they will return a mutilated +/// copy of the data. You can then use your own logic to appropriately +/// update the [TreeView]. e.g. +/// +/// ```dart +/// TreeViewController controller = TreeViewController(children: nodes); +/// Node node = controller.getNode('unique_key')!; +/// Node updatedNode = node.copyWith( +/// key: 'another_unique_key', +/// label: 'Another Node', +/// ); +/// List newChildren = controller.updateNode(node.key, updatedNode); +/// controller = TreeViewController(children: newChildren); +/// ``` +class TreeViewController { + /// The data for the [TreeView]. + final List children; + + /// The key of the select node in the [TreeView]. + final String? selectedKey; + + TreeViewController({ + this.children: const [], + this.selectedKey, + }); + + /// Creates a copy of this controller but with the given fields + /// replaced with the new values. + TreeViewController copyWith({List? children, String? selectedKey}) { + return TreeViewController( + children: children ?? this.children, + selectedKey: selectedKey ?? this.selectedKey, + ); + } + + /// Loads this controller with data from a JSON String + /// This method expects the user to properly update the state + /// + /// ```dart + /// setState((){ + /// controller = controller.loadJSON(json: jsonString); + /// }); + /// ``` + TreeViewController loadJSON({String json: '[]'}) { + List jsonList = jsonDecode(json); + List> list = List>.from(jsonList); + return loadMap(list: list); + } + + /// Loads this controller with data from a Map. + /// This method expects the user to properly update the state + /// + /// ```dart + /// setState((){ + /// controller = controller.loadMap(map: dataMap); + /// }); + /// ``` + TreeViewController loadMap({List> list: const []}) { + List treeData = + list.map((Map item) => Node.fromMap(item)).toList(); + return TreeViewController( + children: treeData, + selectedKey: this.selectedKey, + ); + } + + /// Adds a new node to an existing node identified by specified key. + /// It returns a new controller with the new node added. This method + /// expects the user to properly place this call so that the state is + /// updated. + /// + /// See [TreeViewController.addNode] for info on optional parameters. + /// + /// ```dart + /// setState((){ + /// controller = controller.withAddNode(key, newNode); + /// }); + /// ``` + TreeViewController withAddNode( + String key, + Node newNode, { + Node? parent, + int? index, + InsertMode mode: InsertMode.append, + }) { + List _data = + addNode(key, newNode, parent: parent, mode: mode, index: index); + return TreeViewController( + children: _data, + selectedKey: this.selectedKey, + ); + } + + /// Replaces an existing node identified by specified key with a new node. + /// It returns a new controller with the updated node replaced. This method + /// expects the user to properly place this call so that the state is + /// updated. + /// + /// See [TreeViewController.updateNode] for info on optional parameters. + /// + /// ```dart + /// setState((){ + /// controller = controller.withUpdateNode(key, newNode); + /// }); + /// ``` + TreeViewController withUpdateNode(String key, Node newNode, {Node? parent}) { + List _data = updateNode(key, newNode, parent: parent); + return TreeViewController( + children: _data, + selectedKey: this.selectedKey, + ); + } + + /// Removes an existing node identified by specified key. + /// It returns a new controller with the node removed. This method + /// expects the user to properly place this call so that the state is + /// updated. + /// + /// See [TreeViewController.deleteNode] for info on optional parameters. + /// + /// ```dart + /// setState((){ + /// controller = controller.withDeleteNode(key); + /// }); + /// ``` + TreeViewController withDeleteNode(String key, {Node? parent}) { + List _data = deleteNode(key, parent: parent); + return TreeViewController( + children: _data, + selectedKey: this.selectedKey, + ); + } + + /// Toggles the expanded property of an existing node identified by + /// specified key. It returns a new controller with the node toggled. + /// This method expects the user to properly place this call so + /// that the state is updated. + /// + /// See [TreeViewController.toggleNode] for info on optional parameters. + /// + /// ```dart + /// setState((){ + /// controller = controller.withToggleNode(key, newNode); + /// }); + /// ``` + TreeViewController withToggleNode(String key, {Node? parent}) { + List _data = toggleNode(key, parent: parent); + return TreeViewController( + children: _data, + selectedKey: this.selectedKey, + ); + } + + /// Expands all nodes down to Node identified by specified key. + /// It returns a new controller with the nodes expanded. + /// This method expects the user to properly place this call so + /// that the state is updated. + /// + /// Internally uses [TreeViewController.expandToNode]. + /// + /// ```dart + /// setState((){ + /// controller = controller.withExpandToNode(key, newNode); + /// }); + /// ``` + TreeViewController withExpandToNode(String key) { + List _data = expandToNode(key); + return TreeViewController( + children: _data, + selectedKey: this.selectedKey, + ); + } + + /// Collapses all nodes down to Node identified by specified key. + /// It returns a new controller with the nodes collapsed. + /// This method expects the user to properly place this call so + /// that the state is updated. + /// + /// Internally uses [TreeViewController.collapseToNode]. + /// + /// ```dart + /// setState((){ + /// controller = controller.withCollapseToNode(key, newNode); + /// }); + /// ``` + TreeViewController withCollapseToNode(String key) { + List _data = collapseToNode(key); + return TreeViewController( + children: _data, + selectedKey: this.selectedKey, + ); + } + + /// Expands all nodes down to parent Node. + /// It returns a new controller with the nodes expanded. + /// This method expects the user to properly place this call so + /// that the state is updated. + /// + /// Internally uses [TreeViewController.expandAll]. + /// + /// ```dart + /// setState((){ + /// controller = controller.withExpandAll(); + /// }); + /// ``` + TreeViewController withExpandAll({Node? parent}) { + List _data = expandAll(parent: parent); + return TreeViewController( + children: _data, + selectedKey: this.selectedKey, + ); + } + + /// Collapses all nodes down to parent Node. + /// It returns a new controller with the nodes collapsed. + /// This method expects the user to properly place this call so + /// that the state is updated. + /// + /// Internally uses [TreeViewController.collapseAll]. + /// + /// ```dart + /// setState((){ + /// controller = controller.withCollapseAll(); + /// }); + /// ``` + TreeViewController withCollapseAll({Node? parent}) { + List _data = collapseAll(parent: parent); + return TreeViewController( + children: _data, + selectedKey: this.selectedKey, + ); + } + + /// Gets the node that has a key value equal to the specified key. + Node? getNode(String key, {Node? parent}) { + Node? _found; + List _children = parent == null ? this.children : parent.children; + Iterator iter = _children.iterator; + while (iter.moveNext()) { + Node child = iter.current; + if (child.key == key) { + _found = child; + break; + } else { + if (child.isParent) { + _found = this.getNode(key, parent: child); + if (_found != null) { + break; + } + } + } + } + return _found; + } + + /// Expands all node that are children of the parent node parameter. If no parent is passed, uses the root node as the parent. + List expandAll({Node? parent}) { + List _children = []; + Iterator iter = + parent == null ? this.children.iterator : parent.children.iterator; + while (iter.moveNext()) { + Node child = iter.current; + if (child.isParent) { + _children.add(child.copyWith( + expanded: true, + children: this.expandAll(parent: child), + )); + } else { + _children.add(child); + } + } + return _children; + } + + /// Collapses all node that are children of the parent node parameter. If no parent is passed, uses the root node as the parent. + List collapseAll({Node? parent}) { + List _children = []; + Iterator iter = + parent == null ? this.children.iterator : parent.children.iterator; + while (iter.moveNext()) { + Node child = iter.current; + if (child.isParent) { + _children.add(child.copyWith( + expanded: false, + children: this.expandAll(parent: child), + )); + } else { + _children.add(child); + } + } + return _children; + } + + /// Gets the parent of the node identified by specified key. + Node? getParent(String key, {Node? parent}) { + Node? _found; + List _children = parent == null ? this.children : parent.children; + Iterator iter = _children.iterator; + while (iter.moveNext()) { + Node child = iter.current; + if (child.key == key) { + _found = parent ?? child; + break; + } else { + if (child.isParent) { + _found = this.getParent(key, parent: child); + if (_found != null) { + break; + } + } + } + } + return _found; + } + + /// Expands a node and all of the node's ancestors so that the node is + /// visible without the need to manually expand each node. + List expandToNode(String key) { + List _ancestors = []; + String _currentKey = key; + + _ancestors.add(_currentKey); + + Node? _parent = this.getParent(_currentKey); + if (_parent != null) { + while (_parent!.key != _currentKey) { + _currentKey = _parent.key; + _ancestors.add(_currentKey); + _parent = this.getParent(_currentKey); + } + TreeViewController _this = this; + _ancestors.forEach((String k) { + Node _node = _this.getNode(k)!; + Node _updated = _node.copyWith(expanded: true); + _this = _this.withUpdateNode(k, _updated); + }); + return _this.children; + } + return this.children; + } + + /// Collapses a node and all of the node's ancestors without the need to + /// manually collapse each node. + List collapseToNode(String key) { + List _ancestors = []; + String _currentKey = key; + + _ancestors.add(_currentKey); + + Node? _parent = this.getParent(_currentKey); + if (_parent != null) { + while (_parent!.key != _currentKey) { + _currentKey = _parent.key; + _ancestors.add(_currentKey); + _parent = this.getParent(_currentKey); + } + TreeViewController _this = this; + _ancestors.forEach((String k) { + Node _node = _this.getNode(k)!; + Node _updated = _node.copyWith(expanded: false); + _this = _this.withUpdateNode(k, _updated); + }); + return _this.children; + } + return this.children; + } + + /// Adds a new node to an existing node identified by specified key. It optionally + /// accepts an [InsertMode] and index. If no [InsertMode] is specified, + /// it appends the new node as a child at the end. This method returns + /// a new list with the added node. + List addNode( + String key, + Node newNode, { + Node? parent, + int? index, + InsertMode mode: InsertMode.append, + }) { + List _children = parent == null ? this.children : parent.children; + return _children.map((Node child) { + if (child.key == key) { + List _children = child.children.toList(growable: true); + if (mode == InsertMode.prepend) { + _children.insert(0, newNode); + } else if (mode == InsertMode.insert) { + _children.insert(index ?? _children.length, newNode); + } else { + _children.add(newNode); + } + return child.copyWith(children: _children); + } else { + return child.copyWith( + children: addNode( + key, + newNode, + parent: child, + mode: mode, + index: index, + ), + ); + } + }).toList(); + } + + /// Updates an existing node identified by specified key. This method + /// returns a new list with the updated node. + List updateNode(String key, Node newNode, {Node? parent}) { + List _children = parent == null ? this.children : parent.children; + return _children.map((Node child) { + if (child.key == key) { + return newNode; + } else { + if (child.isParent) { + return child.copyWith( + children: updateNode( + key, + newNode, + parent: child, + ), + ); + } + return child; + } + }).toList(); + } + + /// Toggles an existing node identified by specified key. This method + /// returns a new list with the specified node toggled. + List toggleNode(String key, {Node? parent}) { + Node? _node = getNode(key, parent: parent); + return updateNode(key, _node!.copyWith(expanded: !_node.expanded)); + } + + /// Deletes an existing node identified by specified key. This method + /// returns a new list with the specified node removed. + List deleteNode(String key, {Node? parent}) { + List _children = parent == null ? this.children : parent.children; + List _filteredChildren = []; + Iterator iter = _children.iterator; + while (iter.moveNext()) { + Node child = iter.current; + if (child.key != key) { + if (child.isParent) { + _filteredChildren.add(child.copyWith( + children: deleteNode(key, parent: child), + )); + } else { + _filteredChildren.add(child); + } + } + } + return _filteredChildren; + } + + /// Get the current selected node. Returns null if there is no selectedKey + Node? get selectedNode { + return this.selectedKey!.isEmpty ? null : getNode(this.selectedKey!); + } + + /// Map representation of this object + List> get asMap { + return children.map((Node child) => child.asMap).toList(); + } + + @override + String toString() { + return jsonEncode(asMap); + } +} diff --git a/lib/src/tree_view_theme.dart b/lib/src/tree_view_theme.dart new file mode 100644 index 0000000..ff47a04 --- /dev/null +++ b/lib/src/tree_view_theme.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; + +import 'expander_theme_data.dart'; +import 'tree_node.dart'; + +const double _kDefaultLevelPadding = 20; +const int _kExpandSpeed = 130; + +/// Defines the appearance of the [TreeView]. +/// +/// Used by [TreeView] to control the appearance of the sub-widgets +/// in the [TreeView] widget. +class TreeViewTheme { + /// The [ColorScheme] for [TreeView] widget. + final ColorScheme colorScheme; + + /// The horizontal padding for the children of a [TreeNode] parent. + final double levelPadding; + + /// Whether the [TreeNode] is vertically dense. + /// + /// If this property is null then its value is based on [ListTileTheme.dense]. + /// + /// A dense [TreeNode] defaults to a smaller height. + final bool dense; + + /// Vertical spacing between tabs. + /// If this property is null then [dense] attribute will work and vice versa. + final double? verticalSpacing; + + /// Horizontal spacing between tabs. + /// If this property is null then horizontal spacing between tabs is default [_treeView.theme.iconTheme.size + 5] + final double? horizontalSpacing; + + /// Horizontal padding for node icons. + final double iconPadding; + + /// The default appearance theme for [TreeNode] icons. + final IconThemeData iconTheme; + + /// The appearance theme for [TreeNode] expander icons. + final ExpanderThemeData expanderTheme; + + /// The text style for child [TreeNode] text. + final TextStyle labelStyle; + + /// The text style for parent [TreeNode] text. + final TextStyle parentLabelStyle; + + /// The text overflow for child [TreeNode] text. + /// If this property is null then [softWrap] is true; + final TextOverflow? labelOverflow; + + /// The text overflow for parent [TreeNode] text. + /// If this property is null then [softWrap] is true; + final TextOverflow? parentLabelOverflow; + + /// the speed at which expander icon animates. + final Duration expandSpeed; + + const TreeViewTheme({ + this.colorScheme: const ColorScheme.light(), + this.iconTheme: const IconThemeData.fallback(), + this.expanderTheme: const ExpanderThemeData.fallback(), + this.labelStyle: const TextStyle(), + this.parentLabelStyle: const TextStyle(fontWeight: FontWeight.bold), + this.labelOverflow, + this.parentLabelOverflow, + this.levelPadding: _kDefaultLevelPadding, + this.dense: true, + this.verticalSpacing, + this.horizontalSpacing, + this.iconPadding: 8, + this.expandSpeed: const Duration(milliseconds: _kExpandSpeed), + }); + + /// Creates a [TreeView] theme with some reasonable default values. + /// + /// The [colorScheme] is [ColorScheme.light], + /// the [iconTheme] is [IconThemeData.fallback], + /// the [expanderTheme] is [ExpanderThemeData.fallback], + /// the [labelStyle] is the default [TextStyle], + /// the [parentLabelStyle] is the default [TextStyle] with bold weight, + /// and the default [levelPadding] is 20.0. + const TreeViewTheme.fallback() + : colorScheme = const ColorScheme.light(), + iconTheme = const IconThemeData.fallback(), + expanderTheme = const ExpanderThemeData.fallback(), + labelStyle = const TextStyle(), + parentLabelStyle = const TextStyle(fontWeight: FontWeight.bold), + labelOverflow = null, + parentLabelOverflow = null, + dense = true, + verticalSpacing = null, + horizontalSpacing = null, + iconPadding = 8, + levelPadding = _kDefaultLevelPadding, + expandSpeed = const Duration(milliseconds: _kExpandSpeed); + + /// Creates a copy of this theme but with the given fields replaced with + /// the new values. + TreeViewTheme copyWith({ + ColorScheme? colorScheme, + IconThemeData? iconTheme, + ExpanderThemeData? expanderTheme, + TextStyle? labelStyle, + TextStyle? parentLabelStyle, + TextOverflow? labelOverflow, + TextOverflow? parentLabelOverflow, + bool? dense, + double? verticalSpacing, + double? horizontalSpacing, + double? iconPadding, + double? levelPadding, + }) { + return TreeViewTheme( + colorScheme: colorScheme ?? this.colorScheme, + levelPadding: levelPadding ?? this.levelPadding, + iconPadding: iconPadding ?? this.iconPadding, + iconTheme: iconTheme ?? this.iconTheme, + expanderTheme: expanderTheme ?? this.expanderTheme, + labelStyle: labelStyle ?? this.labelStyle, + dense: dense ?? this.dense, + verticalSpacing: verticalSpacing ?? this.verticalSpacing, + horizontalSpacing: horizontalSpacing ?? this.horizontalSpacing, + parentLabelStyle: parentLabelStyle ?? this.parentLabelStyle, + labelOverflow: labelOverflow ?? this.labelOverflow, + parentLabelOverflow: parentLabelOverflow ?? this.parentLabelOverflow); + } + + /// Returns a new theme that matches this [TreeView] theme but with some values + /// replaced by the non-null parameters of the given icon theme. If the given + /// [TreeViewTheme] is null, simply returns this theme. + TreeViewTheme merge(TreeViewTheme other) { + if (other == null) return this; + return copyWith( + colorScheme: other.colorScheme, + levelPadding: other.levelPadding, + iconPadding: other.iconPadding, + iconTheme: other.iconTheme, + expanderTheme: other.expanderTheme, + labelStyle: other.labelStyle, + dense: other.dense, + verticalSpacing: other.verticalSpacing, + horizontalSpacing: other.horizontalSpacing, + parentLabelStyle: other.parentLabelStyle, + labelOverflow: other.labelOverflow, + parentLabelOverflow: other.parentLabelOverflow); + } + + TreeViewTheme resolve(BuildContext context) => this; + + Duration get quickExpandSpeed => + Duration(milliseconds: (expandSpeed.inMilliseconds * 1.6).toInt()); + + @override + int get hashCode { + return hashValues( + colorScheme, + levelPadding, + iconPadding, + iconTheme, + expanderTheme, + labelStyle, + dense, + verticalSpacing, + horizontalSpacing, + parentLabelStyle, + labelOverflow, + parentLabelOverflow); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is TreeViewTheme && + other.colorScheme == colorScheme && + other.levelPadding == levelPadding && + other.iconPadding == iconPadding && + other.iconTheme == iconTheme && + other.expanderTheme == expanderTheme && + other.labelStyle == labelStyle && + other.dense == dense && + other.verticalSpacing == verticalSpacing && + other.horizontalSpacing == horizontalSpacing && + other.parentLabelStyle == parentLabelStyle && + other.labelOverflow == labelOverflow && + other.parentLabelOverflow == parentLabelOverflow; + } +} diff --git a/lib/src/utilities.dart b/lib/src/utilities.dart new file mode 100644 index 0000000..d83d28f --- /dev/null +++ b/lib/src/utilities.dart @@ -0,0 +1,681 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class Utilities { + static final RegExp _hexExp = RegExp( + r'^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$', + caseSensitive: false, + ); + static final RegExp _rgbExp = RegExp( + r'^rgb\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\)$', + caseSensitive: false, + ); + static final RegExp _rgbaExp = RegExp( + r'^rgba\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|0?\.\d|1(\.0)?)\)$', + caseSensitive: false, + ); + static final RegExp _materialDesignColorExp = RegExp( + r'^((?:red|pink|purple|deepPurple|indigo|blue|lightBlue|cyan|teal|green|lightGreen|lime|yellow|amber|orange|deepOrange|brown|grey|blueGrey)(?:50|100|200|300|400|500|600|700|800|900)?|(?:red|pink|purple|deepPurple|indigo|blue|lightBlue|cyan|teal|green|lightGreen|lime|yellow|amber|orange|deepOrange)(?:Accent|Accent50|Accent100|Accent200|Accent400|Accent700)?|(?:black|white))$', + caseSensitive: false, + ); + + static const Color BLACK = Color.fromARGB(255, 0, 0, 0); + static const Color WHITE = Color.fromARGB(255, 255, 255, 255); + + static Color getColor(String value) { + String colorValue = value; + if (_hexExp.hasMatch(colorValue)) { + final buffer = StringBuffer(); + if (colorValue.length == 3 || colorValue.length == 4) { + colorValue = colorValue.replaceFirst('#', ''); + List pieces = + colorValue.split('').map((String piece) => '$piece$piece').toList(); + colorValue = pieces.join(); + } + if (colorValue.length == 6 || colorValue.length == 7) buffer.write('ff'); + buffer.write(colorValue.replaceFirst('#', '')); + return Color(int.parse(buffer.toString(), radix: 16)); + } else if (_rgbExp.hasMatch(value)) { + var parts = _rgbExp.allMatches(value); + int r = 0; + int g = 0; + int b = 0; + for (var part in parts) { + r = int.parse(part.group(1)!); + g = int.parse(part.group(2)!); + b = int.parse(part.group(3)!); + } + return Color.fromARGB(255, r, g, b); + } else if (_rgbaExp.hasMatch(value)) { + var parts = _rgbaExp.allMatches(value); + int r = 0; + int g = 0; + int b = 0; + double a = 1; + for (var part in parts) { + r = int.parse(part.group(1)!); + g = int.parse(part.group(2)!); + b = int.parse(part.group(3)!); + a = double.parse(part.group(4)!); + } + return Color.fromARGB((255 * a).toInt(), r, g, b); + } else if (_materialDesignColorExp.hasMatch(value)) { + switch (value) { + case 'black': + return Colors.black; + case 'white': + return Colors.white; + case 'amber': + return Colors.amber; + case 'amber100': + return Colors.amber.shade100; + case 'amber200': + return Colors.amber.shade200; + case 'amber300': + return Colors.amber.shade300; + case 'amber400': + return Colors.amber.shade400; + case 'amber500': + return Colors.amber.shade500; + case 'amber600': + return Colors.amber.shade600; + case 'amber700': + return Colors.amber.shade700; + case 'amber800': + return Colors.amber.shade800; + case 'amber900': + return Colors.amber.shade900; + case 'amberAccent': + return Colors.amberAccent; + case 'amberAccent50': + return Colors.amberAccent.shade50; + case 'amberAccent100': + return Colors.amberAccent.shade100; + case 'amberAccent200': + return Colors.amberAccent.shade200; + case 'amberAccent400': + return Colors.amberAccent.shade400; + case 'amberAccent700': + return Colors.amberAccent.shade700; + case 'blue': + return Colors.blue; + case 'blue100': + return Colors.blue.shade100; + case 'blue200': + return Colors.blue.shade200; + case 'blue300': + return Colors.blue.shade300; + case 'blue400': + return Colors.blue.shade400; + case 'blue500': + return Colors.blue.shade500; + case 'blue600': + return Colors.blue.shade600; + case 'blue700': + return Colors.blue.shade700; + case 'blue800': + return Colors.blue.shade800; + case 'blue900': + return Colors.blue.shade900; + case 'blueAccent': + return Colors.blueAccent; + case 'blueAccent50': + return Colors.blueAccent.shade50; + case 'blueAccent100': + return Colors.blueAccent.shade100; + case 'blueAccent200': + return Colors.blueAccent.shade200; + case 'blueAccent400': + return Colors.blueAccent.shade400; + case 'blueAccent700': + return Colors.blueAccent.shade700; + case 'blueGrey': + return Colors.blueGrey; + case 'blueGrey100': + return Colors.blueGrey.shade100; + case 'blueGrey200': + return Colors.blueGrey.shade200; + case 'blueGrey300': + return Colors.blueGrey.shade300; + case 'blueGrey400': + return Colors.blueGrey.shade400; + case 'blueGrey500': + return Colors.blueGrey.shade500; + case 'blueGrey600': + return Colors.blueGrey.shade600; + case 'blueGrey700': + return Colors.blueGrey.shade700; + case 'blueGrey800': + return Colors.blueGrey.shade800; + case 'blueGrey900': + return Colors.blueGrey.shade900; + case 'brown': + return Colors.brown; + case 'brown100': + return Colors.brown.shade100; + case 'brown200': + return Colors.brown.shade200; + case 'brown300': + return Colors.brown.shade300; + case 'brown400': + return Colors.brown.shade400; + case 'brown500': + return Colors.brown.shade500; + case 'brown600': + return Colors.brown.shade600; + case 'brown700': + return Colors.brown.shade700; + case 'brown800': + return Colors.brown.shade800; + case 'brown900': + return Colors.brown.shade900; + case 'cyan': + return Colors.cyan; + case 'cyan100': + return Colors.cyan.shade100; + case 'cyan200': + return Colors.cyan.shade200; + case 'cyan300': + return Colors.cyan.shade300; + case 'cyan400': + return Colors.cyan.shade400; + case 'cyan500': + return Colors.cyan.shade500; + case 'cyan600': + return Colors.cyan.shade600; + case 'cyan700': + return Colors.cyan.shade700; + case 'cyan800': + return Colors.cyan.shade800; + case 'cyan900': + return Colors.cyan.shade900; + case 'cyanAccent': + return Colors.cyanAccent; + case 'cyanAccent50': + return Colors.cyanAccent.shade50; + case 'cyanAccent100': + return Colors.cyanAccent.shade100; + case 'cyanAccent200': + return Colors.cyanAccent.shade200; + case 'cyanAccent400': + return Colors.cyanAccent.shade400; + case 'cyanAccent700': + return Colors.cyanAccent.shade700; + case 'deepOrange': + return Colors.deepOrange; + case 'deepOrange100': + return Colors.deepOrange.shade100; + case 'deepOrange200': + return Colors.deepOrange.shade200; + case 'deepOrange300': + return Colors.deepOrange.shade300; + case 'deepOrange400': + return Colors.deepOrange.shade400; + case 'deepOrange500': + return Colors.deepOrange.shade500; + case 'deepOrange600': + return Colors.deepOrange.shade600; + case 'deepOrange700': + return Colors.deepOrange.shade700; + case 'deepOrange800': + return Colors.deepOrange.shade800; + case 'deepOrange900': + return Colors.deepOrange.shade900; + case 'deepOrangeAccent': + return Colors.deepOrangeAccent; + case 'deepOrangeAccent50': + return Colors.deepOrangeAccent.shade50; + case 'deepOrangeAccent100': + return Colors.deepOrangeAccent.shade100; + case 'deepOrangeAccent200': + return Colors.deepOrangeAccent.shade200; + case 'deepOrangeAccent400': + return Colors.deepOrangeAccent.shade400; + case 'deepOrangeAccent700': + return Colors.deepOrangeAccent.shade700; + case 'deepPurple': + return Colors.deepPurple; + case 'deepPurple100': + return Colors.deepPurple.shade100; + case 'deepPurple200': + return Colors.deepPurple.shade200; + case 'deepPurple300': + return Colors.deepPurple.shade300; + case 'deepPurple400': + return Colors.deepPurple.shade400; + case 'deepPurple500': + return Colors.deepPurple.shade500; + case 'deepPurple600': + return Colors.deepPurple.shade600; + case 'deepPurple700': + return Colors.deepPurple.shade700; + case 'deepPurple800': + return Colors.deepPurple.shade800; + case 'deepPurple900': + return Colors.deepPurple.shade900; + case 'deepPurpleAccent': + return Colors.deepPurpleAccent; + case 'deepPurpleAccent50': + return Colors.deepPurpleAccent.shade50; + case 'deepPurpleAccent100': + return Colors.deepPurpleAccent.shade100; + case 'deepPurpleAccent200': + return Colors.deepPurpleAccent.shade200; + case 'deepPurpleAccent400': + return Colors.deepPurpleAccent.shade400; + case 'deepPurpleAccent700': + return Colors.deepPurpleAccent.shade700; + case 'green': + return Colors.green; + case 'green100': + return Colors.green.shade100; + case 'green200': + return Colors.green.shade200; + case 'green300': + return Colors.green.shade300; + case 'green400': + return Colors.green.shade400; + case 'green500': + return Colors.green.shade500; + case 'green600': + return Colors.green.shade600; + case 'green700': + return Colors.green.shade700; + case 'green800': + return Colors.green.shade800; + case 'green900': + return Colors.green.shade900; + case 'greenAccent': + return Colors.greenAccent; + case 'greenAccent50': + return Colors.greenAccent.shade50; + case 'greenAccent100': + return Colors.greenAccent.shade100; + case 'greenAccent200': + return Colors.greenAccent.shade200; + case 'greenAccent400': + return Colors.greenAccent.shade400; + case 'greenAccent700': + return Colors.greenAccent.shade700; + case 'grey': + return Colors.grey; + case 'grey100': + return Colors.grey.shade100; + case 'grey200': + return Colors.grey.shade200; + case 'grey300': + return Colors.grey.shade300; + case 'grey400': + return Colors.grey.shade400; + case 'grey500': + return Colors.grey.shade500; + case 'grey600': + return Colors.grey.shade600; + case 'grey700': + return Colors.grey.shade700; + case 'grey800': + return Colors.grey.shade800; + case 'grey900': + return Colors.grey.shade900; + case 'indigo': + return Colors.indigo; + case 'indigo100': + return Colors.indigo.shade100; + case 'indigo200': + return Colors.indigo.shade200; + case 'indigo300': + return Colors.indigo.shade300; + case 'indigo400': + return Colors.indigo.shade400; + case 'indigo500': + return Colors.indigo.shade500; + case 'indigo600': + return Colors.indigo.shade600; + case 'indigo700': + return Colors.indigo.shade700; + case 'indigo800': + return Colors.indigo.shade800; + case 'indigo900': + return Colors.indigo.shade900; + case 'indigoAccent': + return Colors.indigoAccent; + case 'indigoAccent50': + return Colors.indigoAccent.shade50; + case 'indigoAccent100': + return Colors.indigoAccent.shade100; + case 'indigoAccent200': + return Colors.indigoAccent.shade200; + case 'indigoAccent400': + return Colors.indigoAccent.shade400; + case 'indigoAccent700': + return Colors.indigoAccent.shade700; + case 'lightBlue': + return Colors.lightBlue; + case 'lightBlue100': + return Colors.lightBlue.shade100; + case 'lightBlue200': + return Colors.lightBlue.shade200; + case 'lightBlue300': + return Colors.lightBlue.shade300; + case 'lightBlue400': + return Colors.lightBlue.shade400; + case 'lightBlue500': + return Colors.lightBlue.shade500; + case 'lightBlue600': + return Colors.lightBlue.shade600; + case 'lightBlue700': + return Colors.lightBlue.shade700; + case 'lightBlue800': + return Colors.lightBlue.shade800; + case 'lightBlue900': + return Colors.lightBlue.shade900; + case 'lightBlueAccent': + return Colors.lightBlueAccent; + case 'lightBlueAccent50': + return Colors.lightBlueAccent.shade50; + case 'lightBlueAccent100': + return Colors.lightBlueAccent.shade100; + case 'lightBlueAccent200': + return Colors.lightBlueAccent.shade200; + case 'lightBlueAccent400': + return Colors.lightBlueAccent.shade400; + case 'lightBlueAccent700': + return Colors.lightBlueAccent.shade700; + case 'lightGreen': + return Colors.lightGreen; + case 'lightGreen100': + return Colors.lightGreen.shade100; + case 'lightGreen200': + return Colors.lightGreen.shade200; + case 'lightGreen300': + return Colors.lightGreen.shade300; + case 'lightGreen400': + return Colors.lightGreen.shade400; + case 'lightGreen500': + return Colors.lightGreen.shade500; + case 'lightGreen600': + return Colors.lightGreen.shade600; + case 'lightGreen700': + return Colors.lightGreen.shade700; + case 'lightGreen800': + return Colors.lightGreen.shade800; + case 'lightGreen900': + return Colors.lightGreen.shade900; + case 'lightGreenAccent': + return Colors.lightGreenAccent; + case 'lightGreenAccent50': + return Colors.lightGreenAccent.shade50; + case 'lightGreenAccent100': + return Colors.lightGreenAccent.shade100; + case 'lightGreenAccent200': + return Colors.lightGreenAccent.shade200; + case 'lightGreenAccent400': + return Colors.lightGreenAccent.shade400; + case 'lightGreenAccent700': + return Colors.lightGreenAccent.shade700; + case 'lime': + return Colors.lime; + case 'lime100': + return Colors.lime.shade100; + case 'lime200': + return Colors.lime.shade200; + case 'lime300': + return Colors.lime.shade300; + case 'lime400': + return Colors.lime.shade400; + case 'lime500': + return Colors.lime.shade500; + case 'lime600': + return Colors.lime.shade600; + case 'lime700': + return Colors.lime.shade700; + case 'lime800': + return Colors.lime.shade800; + case 'lime900': + return Colors.lime.shade900; + case 'limeAccent': + return Colors.limeAccent; + case 'limeAccent50': + return Colors.limeAccent.shade50; + case 'limeAccent100': + return Colors.limeAccent.shade100; + case 'limeAccent200': + return Colors.limeAccent.shade200; + case 'limeAccent400': + return Colors.limeAccent.shade400; + case 'limeAccent700': + return Colors.limeAccent.shade700; + case 'orange': + return Colors.orange; + case 'orange100': + return Colors.orange.shade100; + case 'orange200': + return Colors.orange.shade200; + case 'orange300': + return Colors.orange.shade300; + case 'orange400': + return Colors.orange.shade400; + case 'orange500': + return Colors.orange.shade500; + case 'orange600': + return Colors.orange.shade600; + case 'orange700': + return Colors.orange.shade700; + case 'orange800': + return Colors.orange.shade800; + case 'orange900': + return Colors.orange.shade900; + case 'orangeAccent': + return Colors.orangeAccent; + case 'orangeAccent50': + return Colors.orangeAccent.shade50; + case 'orangeAccent100': + return Colors.orangeAccent.shade100; + case 'orangeAccent200': + return Colors.orangeAccent.shade200; + case 'orangeAccent400': + return Colors.orangeAccent.shade400; + case 'orangeAccent700': + return Colors.orangeAccent.shade700; + case 'pink': + return Colors.pink; + case 'pink100': + return Colors.pink.shade100; + case 'pink200': + return Colors.pink.shade200; + case 'pink300': + return Colors.pink.shade300; + case 'pink400': + return Colors.pink.shade400; + case 'pink500': + return Colors.pink.shade500; + case 'pink600': + return Colors.pink.shade600; + case 'pink700': + return Colors.pink.shade700; + case 'pink800': + return Colors.pink.shade800; + case 'pink900': + return Colors.pink.shade900; + case 'pinkAccent': + return Colors.pinkAccent; + case 'pinkAccent50': + return Colors.pinkAccent.shade50; + case 'pinkAccent100': + return Colors.pinkAccent.shade100; + case 'pinkAccent200': + return Colors.pinkAccent.shade200; + case 'pinkAccent400': + return Colors.pinkAccent.shade400; + case 'pinkAccent700': + return Colors.pinkAccent.shade700; + case 'purple': + return Colors.purple; + case 'purple100': + return Colors.purple.shade100; + case 'purple200': + return Colors.purple.shade200; + case 'purple300': + return Colors.purple.shade300; + case 'purple400': + return Colors.purple.shade400; + case 'purple500': + return Colors.purple.shade500; + case 'purple600': + return Colors.purple.shade600; + case 'purple700': + return Colors.purple.shade700; + case 'purple800': + return Colors.purple.shade800; + case 'purple900': + return Colors.purple.shade900; + case 'purpleAccent': + return Colors.purpleAccent; + case 'purpleAccent50': + return Colors.purpleAccent.shade50; + case 'purpleAccent100': + return Colors.purpleAccent.shade100; + case 'purpleAccent200': + return Colors.purpleAccent.shade200; + case 'purpleAccent400': + return Colors.purpleAccent.shade400; + case 'purpleAccent700': + return Colors.purpleAccent.shade700; + case 'red': + return Colors.red; + case 'red100': + return Colors.red.shade100; + case 'red200': + return Colors.red.shade200; + case 'red300': + return Colors.red.shade300; + case 'red400': + return Colors.red.shade400; + case 'red500': + return Colors.red.shade500; + case 'red600': + return Colors.red.shade600; + case 'red700': + return Colors.red.shade700; + case 'red800': + return Colors.red.shade800; + case 'red900': + return Colors.red.shade900; + case 'redAccent': + return Colors.redAccent; + case 'redAccent50': + return Colors.redAccent.shade50; + case 'redAccent100': + return Colors.redAccent.shade100; + case 'redAccent200': + return Colors.redAccent.shade200; + case 'redAccent400': + return Colors.redAccent.shade400; + case 'redAccent700': + return Colors.redAccent.shade700; + case 'teal': + return Colors.teal; + case 'teal100': + return Colors.teal.shade100; + case 'teal200': + return Colors.teal.shade200; + case 'teal300': + return Colors.teal.shade300; + case 'teal400': + return Colors.teal.shade400; + case 'teal500': + return Colors.teal.shade500; + case 'teal600': + return Colors.teal.shade600; + case 'teal700': + return Colors.teal.shade700; + case 'teal800': + return Colors.teal.shade800; + case 'teal900': + return Colors.teal.shade900; + case 'tealAccent': + return Colors.tealAccent; + case 'tealAccent50': + return Colors.tealAccent.shade50; + case 'tealAccent100': + return Colors.tealAccent.shade100; + case 'tealAccent200': + return Colors.tealAccent.shade200; + case 'tealAccent400': + return Colors.tealAccent.shade400; + case 'tealAccent700': + return Colors.tealAccent.shade700; + case 'yellow': + return Colors.yellow; + case 'yellow100': + return Colors.yellow.shade100; + case 'yellow200': + return Colors.yellow.shade200; + case 'yellow300': + return Colors.yellow.shade300; + case 'yellow400': + return Colors.yellow.shade400; + case 'yellow500': + return Colors.yellow.shade500; + case 'yellow600': + return Colors.yellow.shade600; + case 'yellow700': + return Colors.yellow.shade700; + case 'yellow800': + return Colors.yellow.shade800; + case 'yellow900': + return Colors.yellow.shade900; + case 'yellowAccent': + return Colors.yellowAccent; + case 'yellowAccent50': + return Colors.yellowAccent.shade50; + case 'yellowAccent100': + return Colors.yellowAccent.shade100; + case 'yellowAccent200': + return Colors.yellowAccent.shade200; + case 'yellowAccent400': + return Colors.yellowAccent.shade400; + case 'yellowAccent700': + return Colors.yellowAccent.shade700; + } + } else { + return Color.fromARGB(255, 0, 0, 0); + } + return Color.fromARGB(255, 0, 0, 0); + } + + static String toRGBA(Color color) { + return 'rgba(${color.red},${color.green},${color.blue},${color.alpha / 255})'; + } + + static Color textColor(Color color) { + if (color.computeLuminance() > 0.6) { + return BLACK; + } else { + return WHITE; + } + } + + static String generateRandom([int length = 16]) { + final Random _random = Random.secure(); + var values = List.generate(length, (i) => _random.nextInt(256)); + return base64Url.encode(values).substring(0, length); + } + + static bool truthful(value) { + if (value == null) { + return false; + } + if (value == true || + value == 'true' || + value == 1 || + value == '1' || + value.toString().toLowerCase() == 'yes') { + return true; + } + return false; + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..ad6d1df --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,146 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.10" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" +sdks: + dart: ">=2.12.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..6beff3c --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,23 @@ +name: flutter_treeview +description: A tree widget for Flutter that can be used to display nested, hierarchical data. It includes a number of features like styling labels, icons, and import and export utilities. +version: 1.0.3+2 +homepage: https://bitbucket.org/kevinandre/flutter_treeview/src/master/ +repository: https://bitbucket.org/kevinandre/flutter_treeview/src/master/ +issue_tracker: https://bitbucket.org/kevinandre/flutter_treeview/issues +documentation: https://bitbucket.org/kevinandre/flutter_treeview/wiki/Home +api: https://pub.dev/documentation/flutter_treeview/latest/ +#author: kevinandre.com + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/res/values/strings_en.arb @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/screenshots/ss1.gif b/screenshots/ss1.gif new file mode 100644 index 0000000..81b82c2 Binary files /dev/null and b/screenshots/ss1.gif differ diff --git a/screenshots/ss2.png b/screenshots/ss2.png new file mode 100644 index 0000000..6af8e17 Binary files /dev/null and b/screenshots/ss2.png differ diff --git a/test/expander_theme_data_test.dart b/test/expander_theme_data_test.dart new file mode 100644 index 0000000..e422107 --- /dev/null +++ b/test/expander_theme_data_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_treeview/src/expander_theme_data.dart'; + +void main() { + test('fallback', () { + final ExpanderThemeData theme = ExpanderThemeData.fallback(); + expect(theme.type, ExpanderType.caret); + expect(theme.position, ExpanderPosition.start); + expect(theme.size, 30); + expect(theme.color, Color(0xFF000000)); + }); + test('copyWith', () { + ExpanderThemeData theme = ExpanderThemeData.fallback(); + theme = theme.copyWith( + type: ExpanderType.arrow, + position: ExpanderPosition.end, + size: 20, + color: Color(0xFF990000), + ); + expect(theme.type, ExpanderType.arrow); + expect(theme.position, ExpanderPosition.end); + expect(theme.size, 20); + expect(theme.color, Color(0xFF990000)); + }); + test('merge', () { + ExpanderThemeData theme = ExpanderThemeData.fallback(); + ExpanderThemeData theme2 = ExpanderThemeData( + type: ExpanderType.arrow, + position: ExpanderPosition.end, + modifier: ExpanderModifier.circleFilled, + size: 20, + animated: false, + color: Color(0xFF990000), + ); + theme = theme.merge(theme2); + expect(theme.type, ExpanderType.arrow); + expect(theme.position, ExpanderPosition.end); + expect(theme.modifier, ExpanderModifier.circleFilled); + expect(theme.size, 20); + expect(theme.color, Color(0xFF990000)); + expect(theme, theme2); + }); +} diff --git a/test/node_test.dart b/test/node_test.dart new file mode 100644 index 0000000..d88f39e --- /dev/null +++ b/test/node_test.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_treeview/src/models/node.dart'; + +class Person { + final int age; + + Person({required this.age}); +} + +class Animal { + final int legs; + + Animal({required this.legs}); +} + +void main() { + group('instantiate', () { + test('from string', () { + final Node node = Node.fromLabel('Home'); + expect(node.key.isNotEmpty, true); + expect(node.label, 'Home'); + expect(node.icon, null); + expect(node.expanded, false); + expect(node.children.length, 0); + }); + test('copyWith', () { + final Node node = Node.fromLabel('Home'); + expect(node.label, 'Home'); + expect(node.icon, null); + expect(node.expanded, false); + expect(node.children.length, 0); + final Node newNode = node.copyWith(label: 'New Home'); + expect(newNode.label, 'New Home'); + expect(newNode.icon, null); + expect(newNode.expanded, false); + expect(newNode.children.length, 0); + }); + test('from map', () { + final map = { + "key": "12345", + "label": "Home", + }; + final expectedMap = { + "key": "12345", + "label": "Home", + "icon": null, + "expanded": false, + "parent": false, + "children": [], + }; + final Node node = Node.fromMap(map); + expect(node.key, '12345'); + expect(node.label, 'Home'); + expect(node.icon, null); + expect(node.expanded, false); + expect(node.children.length, 0); + expect(node.isParent, false); + expect(node.asMap, expectedMap); + }); + test('from nested map', () { + final map = { + "label": "Home", + "expanded": 1, + "icon": Icons.home.codePoint.toString(), + "children": [ + { + "key": "12345b", + "label": "Basement", + }, + { + "key": "12345k", + "label": "Kitchen", + } + ], + }; + final expectedMap = { + "label": "Home", + "icon": null, + "expanded": true, + "parent": false, + "children": [ + { + "key": "12345b", + "label": "Basement", + "icon": null, + "expanded": false, + "parent": false, + "children": [], + }, + { + "key": "12345k", + "label": "Kitchen", + "icon": null, + "expanded": false, + "parent": false, + "children": [], + } + ], + }; + final Node node = Node.fromMap(map); + expect(node.key.isNotEmpty, true); + expect(node.label, 'Home'); + expect(node.icon, null); + expect(node.expanded, true); + expect(node.children.length, 2); + expect(node.isParent, true); + Map nodeMap = node.asMap; + nodeMap.remove('key'); + expect(nodeMap, expectedMap); + }); + test('with data', () { + Person lukas = Person(age: 3); + Animal otis = Animal(legs: 4); + final Node node1 = + Node(label: 'Lukas', key: 'lukas', data: lukas); + Node node2 = Node.fromLabel('Friend'); + expect(node1.hasData, true); + expect(node1.data.runtimeType, Person); + + expect(node2.hasData, false); + expect(node2.data.runtimeType, Null); + node2 = node2.copyWith(data: otis); + expect(node2.data.runtimeType, Animal); + expect(node2.hasData, true); + final Node node3 = + Node(label: 'Building Height', key: 'bldghgt', data: 100.4); + expect(node3.hasData, true); + expect(node3.data.runtimeType, double); + }); + }); +} diff --git a/test/tree_view_controller_test.dart b/test/tree_view_controller_test.dart new file mode 100644 index 0000000..40e581c --- /dev/null +++ b/test/tree_view_controller_test.dart @@ -0,0 +1,95 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_treeview/src/models/node.dart'; +import 'package:flutter_treeview/src/tree_view_controller.dart'; + +void main() { + List nodes = [ + Node(label: 'documents', key: 'docs', children: [ + Node(label: 'personal', key: 'd3', children: [ + Node(label: 'Resume.docx', key: 'pd1'), + Node(label: 'Cover Letter.docx', key: 'pd2'), + ]), + Node(label: 'Inspection.docx', key: 'd1'), + Node(label: 'Invoice.docx', key: 'd2'), + ]), + Node( + label: 'MeetingReport.xls', + key: 'mrxls', + ), + Node( + label: 'MeetingReport.pdf', + key: 'mrpdf', + ), + Node( + label: 'Demo.zip', + key: 'demo', + ), + ]; + group('TreeViewController Tests', () { + test('...get node', () { + TreeViewController controller = TreeViewController(children: nodes); + Node? validNode = controller.getNode('pd1'); + Node? invalidNode = controller.getNode('xpd1'); + expect(controller.children.length, 4); + expect(validNode.runtimeType, Node); + expect(validNode!.label, 'Resume.docx'); + expect(invalidNode.runtimeType, Null); + }); + test('...get parent', () { + TreeViewController controller = TreeViewController(children: nodes); + Node? nodeParent = controller.getParent('pd1'); + Node? rootParent = controller.getParent('docs'); + Node? noParent = controller.getParent('xpd1'); + expect(nodeParent.runtimeType, Node); + expect(nodeParent!.key, 'd3'); + expect(rootParent.runtimeType, Node); + expect(rootParent!.key, 'docs'); + expect(noParent.runtimeType, Null); + }); + test('...update node', () { + TreeViewController controller = TreeViewController(children: nodes); + Node? node = controller.getNode('pd1'); + Node updatedNode = node!.copyWith( + key: 'pdf1', + label: 'My Resume.pdf', + ); + List newChildren = controller.updateNode(node.key, updatedNode); + controller = TreeViewController(children: newChildren); + Node? validNode = controller.getNode('pdf1'); + Node? invalidNode = controller.getNode('pd1'); + expect(validNode.runtimeType, Node); + expect(validNode!.key, updatedNode.key); + expect(validNode.label, updatedNode.label); + expect(invalidNode.runtimeType, Null); + }); + test('...delete child node', () { + TreeViewController controller = TreeViewController(children: nodes); + List newChildren = controller.deleteNode('pd1'); + controller = TreeViewController(children: newChildren); + Node? invalidNode = controller.getNode('pd1'); + expect(invalidNode.runtimeType, Null); + }); + test('...delete parent node', () { + TreeViewController controller = TreeViewController(children: nodes); + List newChildren = controller.deleteNode('docs'); + controller = TreeViewController(children: newChildren); + expect(controller.getNode('docs'), null); + expect(controller.getNode('pd1'), null); + expect(controller.getNode('pd2'), null); + expect(controller.getNode('d3'), null); + expect(controller.getNode('d1'), null); + expect(controller.getNode('d2'), null); + }); + test('...add child node', () { + TreeViewController controller = TreeViewController(children: nodes); + Node? invalidNode = controller.getNode('pd3'); + expect(invalidNode.runtimeType, Null); + Node newNode = Node(label: 'References.docx', key: 'pd3'); + List newChildren = controller.addNode('d3', newNode); + controller = TreeViewController(children: newChildren); + Node? validNode = controller.getNode('pd3'); + expect(validNode.runtimeType, Node); + expect(validNode!.label, newNode.label); + }); + }); +} diff --git a/test/tree_view_theme_test.dart b/test/tree_view_theme_test.dart new file mode 100644 index 0000000..3d453d3 --- /dev/null +++ b/test/tree_view_theme_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_treeview/src/expander_theme_data.dart'; +import 'package:flutter_treeview/src/tree_view_theme.dart'; + +void main() { + test('fallback', () { + final TreeViewTheme theme = TreeViewTheme.fallback(); + expect(theme.colorScheme, ColorScheme.light()); + expect(theme.levelPadding, 20); + expect(theme.iconTheme.isConcrete, true); + expect(theme.labelStyle, TextStyle()); + }); + test('copyWith', () { + TreeViewTheme theme = TreeViewTheme.fallback(); + theme = theme.copyWith( + colorScheme: ColorScheme.dark(), + levelPadding: 25, + iconTheme: IconThemeData(), + expanderTheme: ExpanderThemeData(), + labelStyle: TextStyle(fontSize: 35), + ); + expect(theme.colorScheme, ColorScheme.dark()); + expect(theme.levelPadding, 25); + expect(theme.iconTheme.isConcrete, false); + // expect(theme.expanderTheme.isConcrete, true); + expect(theme.labelStyle.fontSize, 35); + }); + test('merge', () { + TreeViewTheme theme = TreeViewTheme.fallback(); + TreeViewTheme theme2 = TreeViewTheme( + colorScheme: ColorScheme.dark(), + levelPadding: 25, + iconTheme: IconThemeData(), + expanderTheme: ExpanderThemeData(), + labelStyle: TextStyle(fontSize: 35), + ); + theme = theme.merge(theme2); + expect(theme.colorScheme, ColorScheme.dark()); + expect(theme.levelPadding, 25); + expect(theme.iconTheme.isConcrete, false); + // expect(theme.expanderTheme.isConcrete, true); + expect(theme.labelStyle.fontSize, 35); + expect(theme, theme2); + }); + test('isConcrete', () { + TreeViewTheme theme = TreeViewTheme.fallback(); + TreeViewTheme theme2 = TreeViewTheme(); + expect(theme == theme2, true); + }); +} diff --git a/test/utilities_color_test.dart b/test/utilities_color_test.dart new file mode 100644 index 0000000..4c9edd8 --- /dev/null +++ b/test/utilities_color_test.dart @@ -0,0 +1,1152 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_treeview/src/utilities.dart'; + +void main() { + group('Color from HEX', () { + test('full with hash', () { + final color = Utilities.getColor('#AAaaaa'); + expect(color, Color(0xffAAAAAA)); + }); + test('full without hash', () { + final color = Utilities.getColor('232323'); + expect(color, Color(0xff232323)); + }); + test('partial with hash', () { + final color = Utilities.getColor('#bcE'); + expect(color, Color(0xffBBCCEE)); + }); + test('partial without hash', () { + final color = Utilities.getColor('981'); + expect(color, Color(0xff998811)); + }); + test('invalid with hash', () { + final color = Utilities.getColor('#4981'); + expect(color, Color(0xff000000)); + }); + test('invalid without hash', () { + final color = Utilities.getColor('981H'); + expect(color, Color(0xff000000)); + }); + }); + group('Color from RGB', () { + test('white', () { + final color = Utilities.getColor('rgb(255,255,255)'); + expect(color, Color(0xffFFFFFF)); + }); + test('black with spaces', () { + final color = Utilities.getColor('rgb(0, 0, 0)'); + expect(color, Color(0xff000000)); + }); + test('invalid', () { + final color = Utilities.getColor('rgb(345)'); + expect(color, Color(0xff000000)); + }); + }); + group('Color from RGBA', () { + test('white', () { + final color = Utilities.getColor('rgba(255,255,255,1)'); + expect(color, Color(0xffFFFFFF)); + }); + test('transparent white', () { + final color = Utilities.getColor('rgba(255,255,255,0)'); + expect(color, Color(0x00FFFFFF)); + }); + test('semi-opaque white', () { + final color = Utilities.getColor('rgba(255,255,255,0.5)'); + expect(color, Color(0x7fFFFFFF)); + }); + test('black with spaces', () { + final color = Utilities.getColor('rgba(0, 0, 0, 1.0)'); + expect(color, Color(0xff000000)); + }); + test('transparent black', () { + final color = Utilities.getColor('rgba(0, 0, 0, 0.0)'); + expect(color, Color(0x00000000)); + }); + test('0.3 transparent black', () { + final color = Utilities.getColor('rgba(0, 0, 0, 0.3)'); + expect(color, Color(0x4c000000)); + }); + test('invalid', () { + final color = Utilities.getColor('rgba(345)'); + expect(color, Color(0xff000000)); + }); + }); + group('Color from Material Design colors', () { + test('white', () { + final color = Utilities.getColor('white'); + expect(color, Colors.white); + }); + test('black', () { + final color = Utilities.getColor('black'); + expect(color, Colors.black); + }); + test('amber', () { + final color = Utilities.getColor('amber'); + expect(color, Colors.amber); + }); + test('amber[100]', () { + final color = Utilities.getColor('amber100'); + expect(color, Colors.amber.shade100); + }); + test('amber[200]', () { + final color = Utilities.getColor('amber200'); + expect(color, Colors.amber.shade200); + }); + test('amber[300]', () { + final color = Utilities.getColor('amber300'); + expect(color, Colors.amber.shade300); + }); + test('amber[400]', () { + final color = Utilities.getColor('amber400'); + expect(color, Colors.amber.shade400); + }); + test('amber[500]', () { + final color = Utilities.getColor('amber500'); + expect(color, Colors.amber.shade500); + }); + test('amber[600]', () { + final color = Utilities.getColor('amber600'); + expect(color, Colors.amber.shade600); + }); + test('amber[700]', () { + final color = Utilities.getColor('amber700'); + expect(color, Colors.amber.shade700); + }); + test('amber[800]', () { + final color = Utilities.getColor('amber800'); + expect(color, Colors.amber.shade800); + }); + test('amber[900]', () { + final color = Utilities.getColor('amber900'); + expect(color, Colors.amber.shade900); + }); + + test('blue', () { + final color = Utilities.getColor('blue'); + expect(color, Colors.blue); + }); + test('blue[100]', () { + final color = Utilities.getColor('blue100'); + expect(color, Colors.blue.shade100); + }); + test('blue[200]', () { + final color = Utilities.getColor('blue200'); + expect(color, Colors.blue.shade200); + }); + test('blue[300]', () { + final color = Utilities.getColor('blue300'); + expect(color, Colors.blue.shade300); + }); + test('blue[400]', () { + final color = Utilities.getColor('blue400'); + expect(color, Colors.blue.shade400); + }); + test('blue[500]', () { + final color = Utilities.getColor('blue500'); + expect(color, Colors.blue.shade500); + }); + test('blue[600]', () { + final color = Utilities.getColor('blue600'); + expect(color, Colors.blue.shade600); + }); + test('blue[700]', () { + final color = Utilities.getColor('blue700'); + expect(color, Colors.blue.shade700); + }); + test('blue[800]', () { + final color = Utilities.getColor('blue800'); + expect(color, Colors.blue.shade800); + }); + test('blue[900]', () { + final color = Utilities.getColor('blue900'); + expect(color, Colors.blue.shade900); + }); + + test('blueGrey', () { + final color = Utilities.getColor('blueGrey'); + expect(color, Colors.blueGrey); + }); + test('blueGrey[100]', () { + final color = Utilities.getColor('blueGrey100'); + expect(color, Colors.blueGrey.shade100); + }); + test('blueGrey[200]', () { + final color = Utilities.getColor('blueGrey200'); + expect(color, Colors.blueGrey.shade200); + }); + test('blueGrey[300]', () { + final color = Utilities.getColor('blueGrey300'); + expect(color, Colors.blueGrey.shade300); + }); + test('blueGrey[400]', () { + final color = Utilities.getColor('blueGrey400'); + expect(color, Colors.blueGrey.shade400); + }); + test('blueGrey[500]', () { + final color = Utilities.getColor('blueGrey500'); + expect(color, Colors.blueGrey.shade500); + }); + test('blueGrey[600]', () { + final color = Utilities.getColor('blueGrey600'); + expect(color, Colors.blueGrey.shade600); + }); + test('blueGrey[700]', () { + final color = Utilities.getColor('blueGrey700'); + expect(color, Colors.blueGrey.shade700); + }); + test('blueGrey[800]', () { + final color = Utilities.getColor('blueGrey800'); + expect(color, Colors.blueGrey.shade800); + }); + test('blueGrey[900]', () { + final color = Utilities.getColor('blueGrey900'); + expect(color, Colors.blueGrey.shade900); + }); + + test('brown', () { + final color = Utilities.getColor('brown'); + expect(color, Colors.brown); + }); + test('brown[100]', () { + final color = Utilities.getColor('brown100'); + expect(color, Colors.brown.shade100); + }); + test('brown[200]', () { + final color = Utilities.getColor('brown200'); + expect(color, Colors.brown.shade200); + }); + test('brown[300]', () { + final color = Utilities.getColor('brown300'); + expect(color, Colors.brown.shade300); + }); + test('brown[400]', () { + final color = Utilities.getColor('brown400'); + expect(color, Colors.brown.shade400); + }); + test('brown[500]', () { + final color = Utilities.getColor('brown500'); + expect(color, Colors.brown.shade500); + }); + test('brown[600]', () { + final color = Utilities.getColor('brown600'); + expect(color, Colors.brown.shade600); + }); + test('brown[700]', () { + final color = Utilities.getColor('brown700'); + expect(color, Colors.brown.shade700); + }); + test('brown[800]', () { + final color = Utilities.getColor('brown800'); + expect(color, Colors.brown.shade800); + }); + test('brown[900]', () { + final color = Utilities.getColor('brown900'); + expect(color, Colors.brown.shade900); + }); + + test('cyan', () { + final color = Utilities.getColor('cyan'); + expect(color, Colors.cyan); + }); + test('cyan[100]', () { + final color = Utilities.getColor('cyan100'); + expect(color, Colors.cyan.shade100); + }); + test('cyan[200]', () { + final color = Utilities.getColor('cyan200'); + expect(color, Colors.cyan.shade200); + }); + test('cyan[300]', () { + final color = Utilities.getColor('cyan300'); + expect(color, Colors.cyan.shade300); + }); + test('cyan[400]', () { + final color = Utilities.getColor('cyan400'); + expect(color, Colors.cyan.shade400); + }); + test('cyan[500]', () { + final color = Utilities.getColor('cyan500'); + expect(color, Colors.cyan.shade500); + }); + test('cyan[600]', () { + final color = Utilities.getColor('cyan600'); + expect(color, Colors.cyan.shade600); + }); + test('cyan[700]', () { + final color = Utilities.getColor('cyan700'); + expect(color, Colors.cyan.shade700); + }); + test('cyan[800]', () { + final color = Utilities.getColor('cyan800'); + expect(color, Colors.cyan.shade800); + }); + test('cyan[900]', () { + final color = Utilities.getColor('cyan900'); + expect(color, Colors.cyan.shade900); + }); + + test('deepOrange', () { + final color = Utilities.getColor('deepOrange'); + expect(color, Colors.deepOrange); + }); + test('deepOrange[100]', () { + final color = Utilities.getColor('deepOrange100'); + expect(color, Colors.deepOrange.shade100); + }); + test('deepOrange[200]', () { + final color = Utilities.getColor('deepOrange200'); + expect(color, Colors.deepOrange.shade200); + }); + test('deepOrange[300]', () { + final color = Utilities.getColor('deepOrange300'); + expect(color, Colors.deepOrange.shade300); + }); + test('deepOrange[400]', () { + final color = Utilities.getColor('deepOrange400'); + expect(color, Colors.deepOrange.shade400); + }); + test('deepOrange[500]', () { + final color = Utilities.getColor('deepOrange500'); + expect(color, Colors.deepOrange.shade500); + }); + test('deepOrange[600]', () { + final color = Utilities.getColor('deepOrange600'); + expect(color, Colors.deepOrange.shade600); + }); + test('deepOrange[700]', () { + final color = Utilities.getColor('deepOrange700'); + expect(color, Colors.deepOrange.shade700); + }); + test('deepOrange[800]', () { + final color = Utilities.getColor('deepOrange800'); + expect(color, Colors.deepOrange.shade800); + }); + test('deepOrange[900]', () { + final color = Utilities.getColor('deepOrange900'); + expect(color, Colors.deepOrange.shade900); + }); + + test('deepPurple', () { + final color = Utilities.getColor('deepPurple'); + expect(color, Colors.deepPurple); + }); + test('deepPurple[100]', () { + final color = Utilities.getColor('deepPurple100'); + expect(color, Colors.deepPurple.shade100); + }); + test('deepPurple[200]', () { + final color = Utilities.getColor('deepPurple200'); + expect(color, Colors.deepPurple.shade200); + }); + test('deepPurple[300]', () { + final color = Utilities.getColor('deepPurple300'); + expect(color, Colors.deepPurple.shade300); + }); + test('deepPurple[400]', () { + final color = Utilities.getColor('deepPurple400'); + expect(color, Colors.deepPurple.shade400); + }); + test('deepPurple[500]', () { + final color = Utilities.getColor('deepPurple500'); + expect(color, Colors.deepPurple.shade500); + }); + test('deepPurple[600]', () { + final color = Utilities.getColor('deepPurple600'); + expect(color, Colors.deepPurple.shade600); + }); + test('deepPurple[700]', () { + final color = Utilities.getColor('deepPurple700'); + expect(color, Colors.deepPurple.shade700); + }); + test('deepPurple[800]', () { + final color = Utilities.getColor('deepPurple800'); + expect(color, Colors.deepPurple.shade800); + }); + test('deepPurple[900]', () { + final color = Utilities.getColor('deepPurple900'); + expect(color, Colors.deepPurple.shade900); + }); + + test('green', () { + final color = Utilities.getColor('green'); + expect(color, Colors.green); + }); + test('green[100]', () { + final color = Utilities.getColor('green100'); + expect(color, Colors.green.shade100); + }); + test('green[200]', () { + final color = Utilities.getColor('green200'); + expect(color, Colors.green.shade200); + }); + test('green[300]', () { + final color = Utilities.getColor('green300'); + expect(color, Colors.green.shade300); + }); + test('green[400]', () { + final color = Utilities.getColor('green400'); + expect(color, Colors.green.shade400); + }); + test('green[500]', () { + final color = Utilities.getColor('green500'); + expect(color, Colors.green.shade500); + }); + test('green[600]', () { + final color = Utilities.getColor('green600'); + expect(color, Colors.green.shade600); + }); + test('green[700]', () { + final color = Utilities.getColor('green700'); + expect(color, Colors.green.shade700); + }); + test('green[800]', () { + final color = Utilities.getColor('green800'); + expect(color, Colors.green.shade800); + }); + test('green[900]', () { + final color = Utilities.getColor('green900'); + expect(color, Colors.green.shade900); + }); + + test('greenAccent[100]', () { + final color = Utilities.getColor('greenAccent100'); + expect(color, Colors.greenAccent.shade100); + }); + test('greenAccent[200]', () { + final color = Utilities.getColor('greenAccent200'); + expect(color, Colors.greenAccent.shade200); + }); + test('greenAccent[400]', () { + final color = Utilities.getColor('greenAccent400'); + expect(color, Colors.greenAccent.shade400); + }); + test('greenAccent[700]', () { + final color = Utilities.getColor('greenAccent700'); + expect(color, Colors.greenAccent.shade700); + }); + + test('grey', () { + final color = Utilities.getColor('grey'); + expect(color, Colors.grey); + }); + test('grey[100]', () { + final color = Utilities.getColor('grey100'); + expect(color, Colors.grey.shade100); + }); + test('grey[200]', () { + final color = Utilities.getColor('grey200'); + expect(color, Colors.grey.shade200); + }); + test('grey[300]', () { + final color = Utilities.getColor('grey300'); + expect(color, Colors.grey.shade300); + }); + test('grey[400]', () { + final color = Utilities.getColor('grey400'); + expect(color, Colors.grey.shade400); + }); + test('grey[500]', () { + final color = Utilities.getColor('grey500'); + expect(color, Colors.grey.shade500); + }); + test('grey[600]', () { + final color = Utilities.getColor('grey600'); + expect(color, Colors.grey.shade600); + }); + test('grey[700]', () { + final color = Utilities.getColor('grey700'); + expect(color, Colors.grey.shade700); + }); + test('grey[800]', () { + final color = Utilities.getColor('grey800'); + expect(color, Colors.grey.shade800); + }); + test('grey[900]', () { + final color = Utilities.getColor('grey900'); + expect(color, Colors.grey.shade900); + }); + + test('indigo', () { + final color = Utilities.getColor('indigo'); + expect(color, Colors.indigo); + }); + test('indigo[100]', () { + final color = Utilities.getColor('indigo100'); + expect(color, Colors.indigo.shade100); + }); + test('indigo[200]', () { + final color = Utilities.getColor('indigo200'); + expect(color, Colors.indigo.shade200); + }); + test('indigo[300]', () { + final color = Utilities.getColor('indigo300'); + expect(color, Colors.indigo.shade300); + }); + test('indigo[400]', () { + final color = Utilities.getColor('indigo400'); + expect(color, Colors.indigo.shade400); + }); + test('indigo[500]', () { + final color = Utilities.getColor('indigo500'); + expect(color, Colors.indigo.shade500); + }); + test('indigo[600]', () { + final color = Utilities.getColor('indigo600'); + expect(color, Colors.indigo.shade600); + }); + test('indigo[700]', () { + final color = Utilities.getColor('indigo700'); + expect(color, Colors.indigo.shade700); + }); + test('indigo[800]', () { + final color = Utilities.getColor('indigo800'); + expect(color, Colors.indigo.shade800); + }); + test('indigo[900]', () { + final color = Utilities.getColor('indigo900'); + expect(color, Colors.indigo.shade900); + }); + + test('lightBlue', () { + final color = Utilities.getColor('lightBlue'); + expect(color, Colors.lightBlue); + }); + test('lightBlue[100]', () { + final color = Utilities.getColor('lightBlue100'); + expect(color, Colors.lightBlue.shade100); + }); + test('lightBlue[200]', () { + final color = Utilities.getColor('lightBlue200'); + expect(color, Colors.lightBlue.shade200); + }); + test('lightBlue[300]', () { + final color = Utilities.getColor('lightBlue300'); + expect(color, Colors.lightBlue.shade300); + }); + test('lightBlue[400]', () { + final color = Utilities.getColor('lightBlue400'); + expect(color, Colors.lightBlue.shade400); + }); + test('lightBlue[500]', () { + final color = Utilities.getColor('lightBlue500'); + expect(color, Colors.lightBlue.shade500); + }); + test('lightBlue[600]', () { + final color = Utilities.getColor('lightBlue600'); + expect(color, Colors.lightBlue.shade600); + }); + test('lightBlue[700]', () { + final color = Utilities.getColor('lightBlue700'); + expect(color, Colors.lightBlue.shade700); + }); + test('lightBlue[800]', () { + final color = Utilities.getColor('lightBlue800'); + expect(color, Colors.lightBlue.shade800); + }); + test('lightBlue[900]', () { + final color = Utilities.getColor('lightBlue900'); + expect(color, Colors.lightBlue.shade900); + }); + + test('lightGreen', () { + final color = Utilities.getColor('lightGreen'); + expect(color, Colors.lightGreen); + }); + test('lightGreen[100]', () { + final color = Utilities.getColor('lightGreen100'); + expect(color, Colors.lightGreen.shade100); + }); + test('lightGreen[200]', () { + final color = Utilities.getColor('lightGreen200'); + expect(color, Colors.lightGreen.shade200); + }); + test('lightGreen[300]', () { + final color = Utilities.getColor('lightGreen300'); + expect(color, Colors.lightGreen.shade300); + }); + test('lightGreen[400]', () { + final color = Utilities.getColor('lightGreen400'); + expect(color, Colors.lightGreen.shade400); + }); + test('lightGreen[500]', () { + final color = Utilities.getColor('lightGreen500'); + expect(color, Colors.lightGreen.shade500); + }); + test('lightGreen[600]', () { + final color = Utilities.getColor('lightGreen600'); + expect(color, Colors.lightGreen.shade600); + }); + test('lightGreen[700]', () { + final color = Utilities.getColor('lightGreen700'); + expect(color, Colors.lightGreen.shade700); + }); + test('lightGreen[800]', () { + final color = Utilities.getColor('lightGreen800'); + expect(color, Colors.lightGreen.shade800); + }); + test('lightGreen[900]', () { + final color = Utilities.getColor('lightGreen900'); + expect(color, Colors.lightGreen.shade900); + }); + + test('lime', () { + final color = Utilities.getColor('lime'); + expect(color, Colors.lime); + }); + test('lime[100]', () { + final color = Utilities.getColor('lime100'); + expect(color, Colors.lime.shade100); + }); + test('lime[200]', () { + final color = Utilities.getColor('lime200'); + expect(color, Colors.lime.shade200); + }); + test('lime[300]', () { + final color = Utilities.getColor('lime300'); + expect(color, Colors.lime.shade300); + }); + test('lime[400]', () { + final color = Utilities.getColor('lime400'); + expect(color, Colors.lime.shade400); + }); + test('lime[500]', () { + final color = Utilities.getColor('lime500'); + expect(color, Colors.lime.shade500); + }); + test('lime[600]', () { + final color = Utilities.getColor('lime600'); + expect(color, Colors.lime.shade600); + }); + test('lime[700]', () { + final color = Utilities.getColor('lime700'); + expect(color, Colors.lime.shade700); + }); + test('lime[800]', () { + final color = Utilities.getColor('lime800'); + expect(color, Colors.lime.shade800); + }); + test('lime[900]', () { + final color = Utilities.getColor('lime900'); + expect(color, Colors.lime.shade900); + }); + + test('orange', () { + final color = Utilities.getColor('orange'); + expect(color, Colors.orange); + }); + test('orange[100]', () { + final color = Utilities.getColor('orange100'); + expect(color, Colors.orange.shade100); + }); + test('orange[200]', () { + final color = Utilities.getColor('orange200'); + expect(color, Colors.orange.shade200); + }); + test('orange[300]', () { + final color = Utilities.getColor('orange300'); + expect(color, Colors.orange.shade300); + }); + test('orange[400]', () { + final color = Utilities.getColor('orange400'); + expect(color, Colors.orange.shade400); + }); + test('orange[500]', () { + final color = Utilities.getColor('orange500'); + expect(color, Colors.orange.shade500); + }); + test('orange[600]', () { + final color = Utilities.getColor('orange600'); + expect(color, Colors.orange.shade600); + }); + test('orange[700]', () { + final color = Utilities.getColor('orange700'); + expect(color, Colors.orange.shade700); + }); + test('orange[800]', () { + final color = Utilities.getColor('orange800'); + expect(color, Colors.orange.shade800); + }); + test('orange[900]', () { + final color = Utilities.getColor('orange900'); + expect(color, Colors.orange.shade900); + }); + + test('pink', () { + final color = Utilities.getColor('pink'); + expect(color, Colors.pink); + }); + test('pink[100]', () { + final color = Utilities.getColor('pink100'); + expect(color, Colors.pink.shade100); + }); + test('pink[200]', () { + final color = Utilities.getColor('pink200'); + expect(color, Colors.pink.shade200); + }); + test('pink[300]', () { + final color = Utilities.getColor('pink300'); + expect(color, Colors.pink.shade300); + }); + test('pink[400]', () { + final color = Utilities.getColor('pink400'); + expect(color, Colors.pink.shade400); + }); + test('pink[500]', () { + final color = Utilities.getColor('pink500'); + expect(color, Colors.pink.shade500); + }); + test('pink[600]', () { + final color = Utilities.getColor('pink600'); + expect(color, Colors.pink.shade600); + }); + test('pink[700]', () { + final color = Utilities.getColor('pink700'); + expect(color, Colors.pink.shade700); + }); + test('pink[800]', () { + final color = Utilities.getColor('pink800'); + expect(color, Colors.pink.shade800); + }); + test('pink[900]', () { + final color = Utilities.getColor('pink900'); + expect(color, Colors.pink.shade900); + }); + + test('purple', () { + final color = Utilities.getColor('purple'); + expect(color, Colors.purple); + }); + test('purple[100]', () { + final color = Utilities.getColor('purple100'); + expect(color, Colors.purple.shade100); + }); + test('purple[200]', () { + final color = Utilities.getColor('purple200'); + expect(color, Colors.purple.shade200); + }); + test('purple[300]', () { + final color = Utilities.getColor('purple300'); + expect(color, Colors.purple.shade300); + }); + test('purple[400]', () { + final color = Utilities.getColor('purple400'); + expect(color, Colors.purple.shade400); + }); + test('purple[500]', () { + final color = Utilities.getColor('purple500'); + expect(color, Colors.purple.shade500); + }); + test('purple[600]', () { + final color = Utilities.getColor('purple600'); + expect(color, Colors.purple.shade600); + }); + test('purple[700]', () { + final color = Utilities.getColor('purple700'); + expect(color, Colors.purple.shade700); + }); + test('purple[800]', () { + final color = Utilities.getColor('purple800'); + expect(color, Colors.purple.shade800); + }); + test('purple[900]', () { + final color = Utilities.getColor('purple900'); + expect(color, Colors.purple.shade900); + }); + + test('red', () { + final color = Utilities.getColor('red'); + expect(color, Colors.red); + }); + test('red[100]', () { + final color = Utilities.getColor('red100'); + expect(color, Colors.red.shade100); + }); + test('red[200]', () { + final color = Utilities.getColor('red200'); + expect(color, Colors.red.shade200); + }); + test('red[300]', () { + final color = Utilities.getColor('red300'); + expect(color, Colors.red.shade300); + }); + test('red[400]', () { + final color = Utilities.getColor('red400'); + expect(color, Colors.red.shade400); + }); + test('red[500]', () { + final color = Utilities.getColor('red500'); + expect(color, Colors.red.shade500); + }); + test('red[600]', () { + final color = Utilities.getColor('red600'); + expect(color, Colors.red.shade600); + }); + test('red[700]', () { + final color = Utilities.getColor('red700'); + expect(color, Colors.red.shade700); + }); + test('red[800]', () { + final color = Utilities.getColor('red800'); + expect(color, Colors.red.shade800); + }); + test('red[900]', () { + final color = Utilities.getColor('red900'); + expect(color, Colors.red.shade900); + }); + + test('teal', () { + final color = Utilities.getColor('teal'); + expect(color, Colors.teal); + }); + test('teal[100]', () { + final color = Utilities.getColor('teal100'); + expect(color, Colors.teal.shade100); + }); + test('teal[200]', () { + final color = Utilities.getColor('teal200'); + expect(color, Colors.teal.shade200); + }); + test('teal[300]', () { + final color = Utilities.getColor('teal300'); + expect(color, Colors.teal.shade300); + }); + test('teal[400]', () { + final color = Utilities.getColor('teal400'); + expect(color, Colors.teal.shade400); + }); + test('teal[500]', () { + final color = Utilities.getColor('teal500'); + expect(color, Colors.teal.shade500); + }); + test('teal[600]', () { + final color = Utilities.getColor('teal600'); + expect(color, Colors.teal.shade600); + }); + test('teal[700]', () { + final color = Utilities.getColor('teal700'); + expect(color, Colors.teal.shade700); + }); + test('teal[800]', () { + final color = Utilities.getColor('teal800'); + expect(color, Colors.teal.shade800); + }); + test('teal[900]', () { + final color = Utilities.getColor('teal900'); + expect(color, Colors.teal.shade900); + }); + + test('yellow', () { + final color = Utilities.getColor('yellow'); + expect(color, Colors.yellow); + }); + test('yellow[100]', () { + final color = Utilities.getColor('yellow100'); + expect(color, Colors.yellow.shade100); + }); + test('yellow[200]', () { + final color = Utilities.getColor('yellow200'); + expect(color, Colors.yellow.shade200); + }); + test('yellow[300]', () { + final color = Utilities.getColor('yellow300'); + expect(color, Colors.yellow.shade300); + }); + test('yellow[400]', () { + final color = Utilities.getColor('yellow400'); + expect(color, Colors.yellow.shade400); + }); + test('yellow[500]', () { + final color = Utilities.getColor('yellow500'); + expect(color, Colors.yellow.shade500); + }); + test('yellow[600]', () { + final color = Utilities.getColor('yellow600'); + expect(color, Colors.yellow.shade600); + }); + test('yellow[700]', () { + final color = Utilities.getColor('yellow700'); + expect(color, Colors.yellow.shade700); + }); + test('yellow[800]', () { + final color = Utilities.getColor('yellow800'); + expect(color, Colors.yellow.shade800); + }); + test('yellow[900]', () { + final color = Utilities.getColor('yellow900'); + expect(color, Colors.yellow.shade900); + }); + test('amberAccent', () { + final color = Utilities.getColor('amberAccent'); + expect(color, Colors.amberAccent); + }); + test('amberAccent[100]', () { + final color = Utilities.getColor('amberAccent100'); + expect(color, Colors.amberAccent.shade100); + }); + test('amberAccent[400]', () { + final color = Utilities.getColor('amberAccent400'); + expect(color, Colors.amberAccent.shade400); + }); + test('amberAccent[700]', () { + final color = Utilities.getColor('amberAccent700'); + expect(color, Colors.amberAccent.shade700); + }); + + test('blueAccent', () { + final color = Utilities.getColor('blueAccent'); + expect(color, Colors.blueAccent); + }); + test('blueAccent[100]', () { + final color = Utilities.getColor('blueAccent100'); + expect(color, Colors.blueAccent.shade100); + }); + test('blueAccent[400]', () { + final color = Utilities.getColor('blueAccent400'); + expect(color, Colors.blueAccent.shade400); + }); + test('blueAccent[700]', () { + final color = Utilities.getColor('blueAccent700'); + expect(color, Colors.blueAccent.shade700); + }); + + test('cyanAccent', () { + final color = Utilities.getColor('cyanAccent'); + expect(color, Colors.cyanAccent); + }); + test('cyanAccent[100]', () { + final color = Utilities.getColor('cyanAccent100'); + expect(color, Colors.cyanAccent.shade100); + }); + test('cyanAccent[400]', () { + final color = Utilities.getColor('cyanAccent400'); + expect(color, Colors.cyanAccent.shade400); + }); + test('cyanAccent[700]', () { + final color = Utilities.getColor('cyanAccent700'); + expect(color, Colors.cyanAccent.shade700); + }); + + test('deepOrangeAccent', () { + final color = Utilities.getColor('deepOrangeAccent'); + expect(color, Colors.deepOrangeAccent); + }); + test('deepOrangeAccent[100]', () { + final color = Utilities.getColor('deepOrangeAccent100'); + expect(color, Colors.deepOrangeAccent.shade100); + }); + test('deepOrangeAccent[400]', () { + final color = Utilities.getColor('deepOrangeAccent400'); + expect(color, Colors.deepOrangeAccent.shade400); + }); + test('deepOrangeAccent[700]', () { + final color = Utilities.getColor('deepOrangeAccent700'); + expect(color, Colors.deepOrangeAccent.shade700); + }); + + test('deepPurpleAccent', () { + final color = Utilities.getColor('deepPurpleAccent'); + expect(color, Colors.deepPurpleAccent); + }); + test('deepPurpleAccent[100]', () { + final color = Utilities.getColor('deepPurpleAccent100'); + expect(color, Colors.deepPurpleAccent.shade100); + }); + test('deepPurpleAccent[400]', () { + final color = Utilities.getColor('deepPurpleAccent400'); + expect(color, Colors.deepPurpleAccent.shade400); + }); + test('deepPurpleAccent[700]', () { + final color = Utilities.getColor('deepPurpleAccent700'); + expect(color, Colors.deepPurpleAccent.shade700); + }); + + test('greenAccent', () { + final color = Utilities.getColor('greenAccent'); + expect(color, Colors.greenAccent); + }); + test('greenAccent[100]', () { + final color = Utilities.getColor('greenAccent100'); + expect(color, Colors.greenAccent.shade100); + }); + test('greenAccent[400]', () { + final color = Utilities.getColor('greenAccent400'); + expect(color, Colors.greenAccent.shade400); + }); + test('greenAccent[700]', () { + final color = Utilities.getColor('greenAccent700'); + expect(color, Colors.greenAccent.shade700); + }); + + test('indigoAccent', () { + final color = Utilities.getColor('indigoAccent'); + expect(color, Colors.indigoAccent); + }); + test('indigoAccent[100]', () { + final color = Utilities.getColor('indigoAccent100'); + expect(color, Colors.indigoAccent.shade100); + }); + test('indigoAccent[400]', () { + final color = Utilities.getColor('indigoAccent400'); + expect(color, Colors.indigoAccent.shade400); + }); + test('indigoAccent[700]', () { + final color = Utilities.getColor('indigoAccent700'); + expect(color, Colors.indigoAccent.shade700); + }); + + test('lightBlueAccent', () { + final color = Utilities.getColor('lightBlueAccent'); + expect(color, Colors.lightBlueAccent); + }); + test('lightBlueAccent[100]', () { + final color = Utilities.getColor('lightBlueAccent100'); + expect(color, Colors.lightBlueAccent.shade100); + }); + test('lightBlueAccent[400]', () { + final color = Utilities.getColor('lightBlueAccent400'); + expect(color, Colors.lightBlueAccent.shade400); + }); + test('lightBlueAccent[700]', () { + final color = Utilities.getColor('lightBlueAccent700'); + expect(color, Colors.lightBlueAccent.shade700); + }); + + test('lightGreenAccent', () { + final color = Utilities.getColor('lightGreenAccent'); + expect(color, Colors.lightGreenAccent); + }); + test('lightGreenAccent[100]', () { + final color = Utilities.getColor('lightGreenAccent100'); + expect(color, Colors.lightGreenAccent.shade100); + }); + test('lightGreenAccent[400]', () { + final color = Utilities.getColor('lightGreenAccent400'); + expect(color, Colors.lightGreenAccent.shade400); + }); + test('lightGreenAccent[700]', () { + final color = Utilities.getColor('lightGreenAccent700'); + expect(color, Colors.lightGreenAccent.shade700); + }); + + test('limeAccent', () { + final color = Utilities.getColor('limeAccent'); + expect(color, Colors.limeAccent); + }); + test('limeAccent[100]', () { + final color = Utilities.getColor('limeAccent100'); + expect(color, Colors.limeAccent.shade100); + }); + test('limeAccent[400]', () { + final color = Utilities.getColor('limeAccent400'); + expect(color, Colors.limeAccent.shade400); + }); + test('limeAccent[700]', () { + final color = Utilities.getColor('limeAccent700'); + expect(color, Colors.limeAccent.shade700); + }); + + test('orangeAccent', () { + final color = Utilities.getColor('orangeAccent'); + expect(color, Colors.orangeAccent); + }); + test('orangeAccent[100]', () { + final color = Utilities.getColor('orangeAccent100'); + expect(color, Colors.orangeAccent.shade100); + }); + test('orangeAccent[400]', () { + final color = Utilities.getColor('orangeAccent400'); + expect(color, Colors.orangeAccent.shade400); + }); + test('orangeAccent[700]', () { + final color = Utilities.getColor('orangeAccent700'); + expect(color, Colors.orangeAccent.shade700); + }); + + test('pinkAccent', () { + final color = Utilities.getColor('pinkAccent'); + expect(color, Colors.pinkAccent); + }); + test('pinkAccent[100]', () { + final color = Utilities.getColor('pinkAccent100'); + expect(color, Colors.pinkAccent.shade100); + }); + test('pinkAccent[400]', () { + final color = Utilities.getColor('pinkAccent400'); + expect(color, Colors.pinkAccent.shade400); + }); + test('pinkAccent[700]', () { + final color = Utilities.getColor('pinkAccent700'); + expect(color, Colors.pinkAccent.shade700); + }); + + test('purpleAccent', () { + final color = Utilities.getColor('purpleAccent'); + expect(color, Colors.purpleAccent); + }); + test('purpleAccent[100]', () { + final color = Utilities.getColor('purpleAccent100'); + expect(color, Colors.purpleAccent.shade100); + }); + test('purpleAccent[400]', () { + final color = Utilities.getColor('purpleAccent400'); + expect(color, Colors.purpleAccent.shade400); + }); + test('purpleAccent[700]', () { + final color = Utilities.getColor('purpleAccent700'); + expect(color, Colors.purpleAccent.shade700); + }); + + test('redAccent', () { + final color = Utilities.getColor('redAccent'); + expect(color, Colors.redAccent); + }); + test('redAccent[100]', () { + final color = Utilities.getColor('redAccent100'); + expect(color, Colors.redAccent.shade100); + }); + test('redAccent[400]', () { + final color = Utilities.getColor('redAccent400'); + expect(color, Colors.redAccent.shade400); + }); + test('redAccent[700]', () { + final color = Utilities.getColor('redAccent700'); + expect(color, Colors.redAccent.shade700); + }); + + test('tealAccent', () { + final color = Utilities.getColor('tealAccent'); + expect(color, Colors.tealAccent); + }); + test('tealAccent[100]', () { + final color = Utilities.getColor('tealAccent100'); + expect(color, Colors.tealAccent.shade100); + }); + test('tealAccent[400]', () { + final color = Utilities.getColor('tealAccent400'); + expect(color, Colors.tealAccent.shade400); + }); + test('tealAccent[700]', () { + final color = Utilities.getColor('tealAccent700'); + expect(color, Colors.tealAccent.shade700); + }); + + test('yellowAccent', () { + final color = Utilities.getColor('yellowAccent'); + expect(color, Colors.yellowAccent); + }); + test('yellowAccent[100]', () { + final color = Utilities.getColor('yellowAccent100'); + expect(color, Colors.yellowAccent.shade100); + }); + test('yellowAccent[400]', () { + final color = Utilities.getColor('yellowAccent400'); + expect(color, Colors.yellowAccent.shade400); + }); + test('yellowAccent[700]', () { + final color = Utilities.getColor('yellowAccent700'); + expect(color, Colors.yellowAccent.shade700); + }); + }); +} diff --git a/test/utilities_test.dart b/test/utilities_test.dart new file mode 100644 index 0000000..ef9bf49 --- /dev/null +++ b/test/utilities_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_treeview/src/utilities.dart'; + +void main() { + group('random generator', () { + test('generates at least 300 unique strings', () { + List _keys = []; + final int count = 300; + for (int i = 0; i < count; i++) { + _keys.add(Utilities.generateRandom()); + } + expect(_keys.length, count); + expect(_keys, _keys.toSet().toList()); + }); + test('generates at least 200 unique 10 character strings', () { + List _keys = []; + final int count = 200; + for (int i = 0; i < count; i++) { + _keys.add(Utilities.generateRandom(10)); + } + expect(_keys.first.length, 10); + expect(_keys.length, count); + expect(_keys, _keys.toSet().toList()); + }); + }); + group('truthful', () { + test('is true', () { + expect(Utilities.truthful(true), true); + expect(Utilities.truthful('true'), true); + expect(Utilities.truthful(1), true); + expect(Utilities.truthful('1'), true); + expect(Utilities.truthful('yes'), true); + }); + test('is false', () { + expect(Utilities.truthful(false), false); + expect(Utilities.truthful('false'), false); + expect(Utilities.truthful(0), false); + expect(Utilities.truthful('0'), false); + expect(Utilities.truthful('no'), false); + expect(Utilities.truthful('someothervalue'), false); + }); + }); +}