diff --git a/CHANGELOG.md b/CHANGELOG.md index 6073234..fcf785e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 0.3.0 + +* BREAKING: formatters such as `valueFormatter` now takes node in addition to value +* BREAKING: `valueStyleBuilder` now takes node in addition to value +* `style` in `PropertyOverrides` is now optional +* `PropertyOverrides` can optionally specify `onSecondaryTap`, `onLongPress` and a mouse `cursor` +* Upgrade `golden_toolkit` to 0.15.0 +* Upgrade example dependencies + +## 0.2.0 + +* Upgrade `scrollable_positioned_list` to 0.3.2 +* Upgrade `provider` to 6.0.3 +* Upgrade `rows_lint` to 0.1.1 + ## 0.1.0 * Initial release. diff --git a/README.md b/README.md index 2d8f6a2..f0d63cd 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ can be changed with a formatter: ```dart JsonDataExplorer( nodes: state.displayNodes, - propertyNameFormatter: (name) => '$name ->', + propertyNameFormatter: (node, name) => '$name ->', ) ``` @@ -183,7 +183,7 @@ An example is adding interaction to values that contains links: ```dart JsonDataExplorer( nodes: state.displayNodes, - valueStyleBuilder: (value, style) { + valueStyleBuilder: (node, value, style) { final isUrl = _valueIsUrl(value); return PropertyOverrides( style: isUrl @@ -203,7 +203,7 @@ value types: ```dart JsonDataExplorer( nodes: state.displayNodes, - valueStyleBuilder: (value, style) { + valueStyleBuilder: (node, value, style) { if (value is num) { return PropertyOverrides( style: style.copyWith( diff --git a/example/lib/main.dart b/example/lib/main.dart index e40319f..09c66c2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -254,11 +254,11 @@ class _DataExplorerPageState extends State<DataExplorerPage> { : const SizedBox(), /// Creates a custom format for classes and array names. - rootNameFormatter: (dynamic name) => '$name', + rootNameFormatter: (dynamic node, dynamic name) => '$name', /// Dynamically changes the property value style and /// interaction when an URL is detected. - valueStyleBuilder: (dynamic value, style) { + valueStyleBuilder: (node, dynamic value, style) { final isUrl = _valueIsUrl(value); return PropertyOverrides( style: isUrl @@ -267,6 +267,12 @@ class _DataExplorerPageState extends State<DataExplorerPage> { ) : style, onTap: isUrl ? () => _launchUrl(value as String) : null, + onSecondaryTap: () { + print('onSecondaryTap: ${node.key}: $value'); + }, + onLongPress: () { + print('onLongPress: ${node.key}: $value'); + }, ); }, diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 4618f38..a1cdfd0 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,7 @@ import FlutterMacOS import Foundation -import path_provider_macos +import path_provider_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { diff --git a/example/pubspec.lock b/example/pubspec.lock index 3529304..1f94d79 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,79 +5,82 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" effective_dart: dependency: transitive description: name: effective_dart - url: "https://pub.dartlang.org" + sha256: "6a69783c808344084b65667e87ff600823531e95810a8a15882cb542fe22de80" + url: "https://pub.dev" source: hosted version: "1.3.2" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.2" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" flutter: dependency: "direct main" description: flutter @@ -97,163 +100,177 @@ packages: dependency: "direct main" description: name: google_fonts - url: "https://pub.dartlang.org" + sha256: "6b6f10f0ce3c42f6552d1c70d2c28d764cf22bb487f50f66cca31dcd5194f4d6" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "4.0.4" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "0.13.6" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.5" json_data_explorer: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.0.1" + version: "0.3.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.8.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.8.2" path_provider: dependency: transitive description: name: path_provider - url: "https://pub.dartlang.org" + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.15" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + url: "https://pub.dev" source: hosted - version: "2.0.12" - path_provider_ios: + version: "2.0.27" + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - url: "https://pub.dartlang.org" + name: path_provider_foundation + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.2.3" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + url: "https://pub.dev" source: hosted - version: "2.1.5" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.5" + version: "2.1.11" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.7" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" provider: dependency: transitive description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.5" rows_lint: dependency: "direct dev" description: name: rows_lint - url: "https://pub.dartlang.org" + sha256: "772b3e0759a62282365c16ea3e51e2bab5823b916f6ae6f4bfe2012e28028afc" + url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" scrollable_positioned_list: dependency: transitive description: name: scrollable_positioned_list - url: "https://pub.dartlang.org" + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.3.8" sky_engine: dependency: transitive description: flutter @@ -263,128 +280,146 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.8" + version: "0.4.16" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + url: "https://pub.dev" source: hosted - version: "6.0.20" + version: "6.1.11" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: eed4e6a1164aa9794409325c3b707ff424d4d1c2a785e7db67f8bbda00e36e51 + url: "https://pub.dev" source: hosted - version: "6.0.15" + version: "6.0.35" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" source: hosted - version: "6.0.15" + version: "6.1.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.5" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" + url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.17" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "4.1.4" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" source: hosted - version: "0.2.0+1" + version: "1.0.0" sdks: - dart: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + dart: ">=2.19.0 <3.0.0" + flutter: ">=3.3.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 59b8d1c..fd0bd61 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,13 +13,13 @@ dependencies: path: ../ http: ^0.13.4 - google_fonts: ^2.3.1 + google_fonts: ^4.0.4 url_launcher: ^6.0.20 dev_dependencies: flutter_test: sdk: flutter - rows_lint: 0.1.0 + rows_lint: ^0.1.1 flutter: uses-material-design: true diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 411af46..88b22e5 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/lib/src/data_explorer_store.dart b/lib/src/data_explorer_store.dart index 795473d..43ad0cd 100644 --- a/lib/src/data_explorer_store.dart +++ b/lib/src/data_explorer_store.dart @@ -630,9 +630,26 @@ class DataExplorerStore extends ChangeNotifier { /// initially only upper root nodes will be in the list. /// /// [notifyListeners] is called to notify all registered listeners. - Future buildNodes(dynamic jsonObject, {bool areAllCollapsed = false}) async { + /// [mapScalarNodeValue] can optionally change scalar values to be a different + /// value after construction of the nodes and before they are in the store. + Future buildNodes( + dynamic jsonObject, { + bool areAllCollapsed = false, + dynamic Function(NodeViewModelState node)? mapScalarNodeValue, + }) async { final builtNodes = buildViewModelNodes(jsonObject); final flatList = flatten(builtNodes); + if (mapScalarNodeValue != null) { + for (final n in flatList) { + if ((n.value is! Map<String, dynamic>) && (n.value is! List)) { + final dynamic newValue = mapScalarNodeValue(n); + if ((newValue is Map<String, dynamic>) || (newValue is List)) { + throw ArgumentError('mapScalarNodeValue must return a scalar'); + } + n.value = newValue; + } + } + } _allNodes = UnmodifiableListView(flatList); _displayNodes = List.from(flatList); @@ -655,7 +672,7 @@ class DataExplorerStore extends ChangeNotifier { void _doSearch() { for (final node in _allNodes) { - final matchesIndexes = _getSearchTermMatchesIndexes(node.key); + final matchesIndexes = getSearchTermMatchesIndexes(node.key); for (final matchIndex in matchesIndexes) { _searchResults.add( @@ -669,7 +686,7 @@ class DataExplorerStore extends ChangeNotifier { if (!node.isRoot) { final matchesIndexes = - _getSearchTermMatchesIndexes(node.value.toString()); + getSearchTermMatchesIndexes(node.value.toString()); for (final matchIndex in matchesIndexes) { _searchResults.add( @@ -686,14 +703,21 @@ class DataExplorerStore extends ChangeNotifier { notifyListeners(); } - /// Finds all occurences of [searchTerm] in [victim] and retrieves all their + /// Finds all occurrences of [regexp] in [victim] and retrieves all their /// indexes. - Iterable<int> _getSearchTermMatchesIndexes(String victim) { - final pattern = RegExp(searchTerm, caseSensitive: false); - - final matches = pattern.allMatches(victim).map((match) => match.start); + static Iterable<RegExpMatch> getIndexesOfMatches( + String regexp, + String victim, { + bool caseSensitive = false, + }) { + final pattern = RegExp(regexp, caseSensitive: caseSensitive); + return pattern.allMatches(victim); + } - return matches; + /// Finds all occurences of [searchTerm] in [victim] and retrieves all their + /// indexes. + Iterable<int> getSearchTermMatchesIndexes(String victim) { + return getIndexesOfMatches(searchTerm, victim).map((match) => match.start); } /// Expands all the parent nodes of each [SearchResult.node] in diff --git a/lib/src/data_explorer_theme.dart b/lib/src/data_explorer_theme.dart index b874318..95defcc 100644 --- a/lib/src/data_explorer_theme.dart +++ b/lib/src/data_explorer_theme.dart @@ -31,6 +31,12 @@ class DataExplorerTheme { /// If not set falls back to [valueSearchHighlightTextStyle]. final TextStyle focusedValueSearchHighlightTextStyle; + /// Whether or not to highlight only matched groups in the search term regex. + /// This is limited right now to highlighting all substrings that match any + /// of the matching groups, rather than the exact groups, as dart does not + /// yet support providing positions of matched regexp groups. + final bool highlightOnlyRegExpGroups; + /// Indentation lines color. final Color indentationLineColor; @@ -54,6 +60,7 @@ class DataExplorerTheme { TextStyle? valueSearchHighlightTextStyle, TextStyle? focusedKeySearchHighlightTextStyle, TextStyle? focusedValueSearchHighlightTextStyle, + this.highlightOnlyRegExpGroups = false, this.indentationLineColor = Colors.grey, this.highlightColor, this.indentationPadding = 8.0, @@ -88,6 +95,7 @@ class DataExplorerTheme { required this.valueSearchHighlightTextStyle, required this.focusedKeySearchNodeHighlightTextStyle, required this.focusedValueSearchHighlightTextStyle, + required this.highlightOnlyRegExpGroups, required this.indentationLineColor, required this.highlightColor, required this.indentationPadding, @@ -134,6 +142,7 @@ class DataExplorerTheme { fontWeight: FontWeight.bold, backgroundColor: Colors.lightGreen, ), + highlightOnlyRegExpGroups: false, indentationLineColor: Colors.grey, highlightColor: Colors.black12, indentationPadding: 8.0, @@ -148,6 +157,7 @@ class DataExplorerTheme { TextStyle? valueSearchHighlightTextStyle, TextStyle? focusedKeySearchNodeHighlightTextStyle, TextStyle? focusedValueSearchHighlightTextStyle, + bool? highlightOnlyRegExpGroups, Color? indentationLineColor, Color? highlightColor, double? indentationPadding, @@ -172,6 +182,8 @@ class DataExplorerTheme { focusedValueSearchHighlightTextStyle: focusedValueSearchHighlightTextStyle ?? this.focusedValueSearchHighlightTextStyle, + highlightOnlyRegExpGroups: + highlightOnlyRegExpGroups ?? this.highlightOnlyRegExpGroups, ); @override diff --git a/lib/src/json_data_explorer.dart b/lib/src/json_data_explorer.dart index 9620861..657ac31 100644 --- a/lib/src/json_data_explorer.dart +++ b/lib/src/json_data_explorer.dart @@ -12,9 +12,9 @@ typedef NodeBuilder = Widget Function( NodeViewModelState node, ); -/// Signature for a function that takes a generic value and converts it to a -/// string. -typedef Formatter = String Function(dynamic value); +/// Signature for a function that takes a generic value (associated with a +/// given node in the model) and converts it to a string. +typedef Formatter = String Function(NodeViewModelState node, dynamic value); /// Signature for a function that takes a generic value and the current theme /// property value style and returns a [StyleBuilder] that allows the style @@ -23,16 +23,26 @@ typedef Formatter = String Function(dynamic value); /// See also: /// * [PropertyStyle] typedef StyleBuilder = PropertyOverrides Function( + NodeViewModelState node, dynamic value, TextStyle style, ); /// Holds information about a property value style and interaction. class PropertyOverrides { - final TextStyle style; + final TextStyle? style; final VoidCallback? onTap; - - const PropertyOverrides({required this.style, this.onTap}); + final VoidCallback? onSecondaryTap; + final VoidCallback? onLongPress; + final MouseCursor? cursor; + + const PropertyOverrides({ + this.style, + this.onTap, + this.onSecondaryTap, + this.onLongPress, + this.cursor, + }); } /// A widget to display a list of Json nodes. @@ -263,15 +273,18 @@ class JsonAttribute extends StatelessWidget { final valueStyle = valueStyleBuilder != null ? valueStyleBuilder!.call( + node, node.value, theme.valueTextStyle, ) - : PropertyOverrides(style: theme.valueTextStyle); + : const PropertyOverrides(); final hasInteraction = node.isRoot || valueStyle.onTap != null; return MouseRegion( - cursor: hasInteraction ? SystemMouseCursors.click : MouseCursor.defer, + cursor: valueStyle.cursor != null + ? valueStyle.cursor! + : (hasInteraction ? SystemMouseCursors.click : MouseCursor.defer), onEnter: (event) { node.highlight(); node.focus(); @@ -291,6 +304,8 @@ class JsonAttribute extends StatelessWidget { } } : null, + onLongPress: valueStyle.onLongPress, + onSecondaryTap: valueStyle.onSecondaryTap, child: AnimatedBuilder( animation: node, @@ -341,11 +356,13 @@ class JsonAttribute extends StatelessWidget { node: node, searchTerm: searchTerm, valueFormatter: valueFormatter, - style: valueStyle.style, + style: valueStyle.style ?? theme.valueTextStyle, searchHighlightStyle: theme.valueSearchHighlightTextStyle, focusedSearchHighlightStyle: theme.focusedValueSearchHighlightTextStyle, + highlightOnlyRegExpGroups: + theme.highlightOnlyRegExpGroups, ), ), ), @@ -414,9 +431,9 @@ class _RootNodeWidget extends StatelessWidget { String _keyName() { if (node.isRoot) { - return rootNameFormatter?.call(node.key) ?? '${node.key}:'; + return rootNameFormatter?.call(node, node.key) ?? '${node.key}:'; } - return propertyNameFormatter?.call(node.key) ?? '${node.key}:'; + return propertyNameFormatter?.call(node, node.key) ?? '${node.key}:'; } /// Gets the index of the focused search match. @@ -455,13 +472,14 @@ class _RootNodeWidget extends StatelessWidget { final focusedSearchMatchIndex = context.select<DataExplorerStore, int?>(_getFocusedSearchMatchIndex); - return _HighlightedText( + return HighlightedText( text: text, - highlightedText: searchTerm, + highlightedRegExp: searchTerm, style: attributeKeyStyle, primaryMatchStyle: theme.focusedKeySearchNodeHighlightTextStyle, secondaryMatchStyle: theme.keySearchHighlightTextStyle, focusedSearchMatchIndex: focusedSearchMatchIndex, + highlightOnlyRegExpGroups: theme.highlightOnlyRegExpGroups, ); } } @@ -474,6 +492,7 @@ class _PropertyNodeWidget extends StatelessWidget { final TextStyle style; final TextStyle searchHighlightStyle; final TextStyle focusedSearchHighlightStyle; + final bool highlightOnlyRegExpGroups; const _PropertyNodeWidget({ Key? key, @@ -483,6 +502,7 @@ class _PropertyNodeWidget extends StatelessWidget { required this.style, required this.searchHighlightStyle, required this.focusedSearchHighlightStyle, + required this.highlightOnlyRegExpGroups, }) : super(key: key); /// Gets the index of the focused search match. @@ -509,7 +529,8 @@ class _PropertyNodeWidget extends StatelessWidget { (store) => store.searchResults.isNotEmpty, ); - final text = valueFormatter?.call(node.value) ?? node.value.toString(); + final text = + valueFormatter?.call(node, node.value) ?? node.value.toString(); if (!showHighlightedText) { return Text(text, style: style); @@ -518,13 +539,14 @@ class _PropertyNodeWidget extends StatelessWidget { final focusedSearchMatchIndex = context.select<DataExplorerStore, int?>(_getFocusedSearchMatchIndex); - return _HighlightedText( + return HighlightedText( text: text, - highlightedText: searchTerm, + highlightedRegExp: searchTerm, style: style, primaryMatchStyle: focusedSearchHighlightStyle, secondaryMatchStyle: searchHighlightStyle, focusedSearchMatchIndex: focusedSearchMatchIndex, + highlightOnlyRegExpGroups: highlightOnlyRegExpGroups, ); } } @@ -599,11 +621,13 @@ class _Indentation extends StatelessWidget { } } -/// Highlights found occurrences of [highlightedText] with [highlightedStyle] +/// Highlights found occurrences of [highlightedRegExp] with [highlightedStyle] /// in [text]. -class _HighlightedText extends StatelessWidget { +class HighlightedText extends StatelessWidget { final String text; - final String highlightedText; + final String highlightedRegExp; + final bool caseSensitive; + final bool highlightOnlyRegExpGroups; // The default style when the text or part of it is not highlighted. final TextStyle style; @@ -617,10 +641,12 @@ class _HighlightedText extends StatelessWidget { // The index of the focused search match. final int? focusedSearchMatchIndex; - const _HighlightedText({ + const HighlightedText({ Key? key, required this.text, - required this.highlightedText, + required this.highlightedRegExp, + this.caseSensitive = false, + this.highlightOnlyRegExpGroups = false, required this.style, required this.primaryMatchStyle, required this.secondaryMatchStyle, @@ -629,21 +655,54 @@ class _HighlightedText extends StatelessWidget { @override Widget build(BuildContext context) { - final lowerCaseText = text.toLowerCase(); - final lowerCaseQuery = highlightedText.toLowerCase(); - - if (highlightedText.isEmpty || !lowerCaseText.contains(lowerCaseQuery)) { + var matchingIndexes = highlightedRegExp.isEmpty + ? const Iterable<RegExpMatch>.empty() + : DataExplorerStore.getIndexesOfMatches( + highlightedRegExp, + text, + caseSensitive: caseSensitive, + ); + if (matchingIndexes.isEmpty) { return Text(text, style: style); } + // It seems that positions of matching groups are not available for now + // (see https://github.com/dart-lang/sdk/issues/45486). We have to thus + // take a more complex approach if we only want to highlight group contents + // by first finding all matches, and then getting all of the group contents, + // and finding all matches anywhere in the string that could match these + // group contents. This will highlight potentially a lot more than just + // the actual group matches, but is an approximation we'll have to live with + // until the above dart:core enhancement is finished. + if (highlightOnlyRegExpGroups) { + final allGroups = <String>{}; + for (final m in matchingIndexes) { + final groups = m + .groups(List<int>.generate(m.groupCount, (index) => index + 1)) + .map((s) => s ?? '') + .where((s) => s.isNotEmpty); + allGroups.addAll(groups); + } + // for highlighting purposes, any substring that matches a known + // group match should get highlighted. place longer groups first so + // we always greedy match match longer expressions first. + final sortedGroups = allGroups.toList() + ..sort((a, b) => b.length.compareTo(a.length)); + final newRegExp = sortedGroups.map(RegExp.escape).join('|'); + matchingIndexes = DataExplorerStore.getIndexesOfMatches( + newRegExp, + text, + caseSensitive: caseSensitive, + ); + } + final spans = <TextSpan>[]; var start = 0; - while (true) { - var index = lowerCaseText.indexOf(lowerCaseQuery, start); - index = index >= 0 ? index : text.length; + for (final m in matchingIndexes) { + final index = m.start; - if (start != index) { + if (start < index) { spans.add( TextSpan( text: text.substring(start, index), @@ -658,13 +717,22 @@ class _HighlightedText extends StatelessWidget { spans.add( TextSpan( - text: text.substring(index, index + highlightedText.length), + text: text.substring(index, m.end), style: index == focusedSearchMatchIndex ? primaryMatchStyle : secondaryMatchStyle, ), ); - start = index + highlightedText.length; + start = m.end; + } + + if (start != text.length) { + spans.add( + TextSpan( + text: text.substring(start), + style: style, + ), + ); } return Text.rich( diff --git a/pubspec.yaml b/pubspec.yaml index 8d979a8..864d4a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: json_data_explorer description: A highly customizable widget to render and interact with JSON objects. -version: 0.1.0 +version: 0.3.0 repository: https://github.com/rows/json_data_explorer homepage: https://github.com/rows/json_data_explorer issue_tracker: https://github.com/rows/json_data_explorer/issues @@ -12,15 +12,15 @@ environment: dependencies: flutter: sdk: flutter - scrollable_positioned_list: ^0.2.3 - provider: ^6.0.2 + scrollable_positioned_list: ^0.3.2 + provider: ^6.0.3 dev_dependencies: flutter_test: sdk: flutter - rows_lint: 0.1.0 + rows_lint: ^0.1.1 mocktail: ^0.3.0 - golden_toolkit: ^0.13.0 + golden_toolkit: ^0.15.0 flutter: uses-material-design: true \ No newline at end of file diff --git a/test/golden/json_data_explorer_test.dart b/test/golden/json_data_explorer_test.dart index 6f8d2d3..f4fa0f7 100644 --- a/test/golden/json_data_explorer_test.dart +++ b/test/golden/json_data_explorer_test.dart @@ -114,15 +114,15 @@ void main() { ..addScenario( 'Name formatters', buildWidget( - rootNameFormatter: (dynamic name) => '$name', - propertyNameFormatter: (dynamic name) => '$name =', - valueFormatter: (dynamic value) => '"$value"', + rootNameFormatter: (dynamic node, dynamic name) => '$name', + propertyNameFormatter: (dynamic node, dynamic name) => '$name =', + valueFormatter: (dynamic node, dynamic value) => '"$value"', ), ) ..addScenario( 'Value style builder', buildWidget( - valueStyleBuilder: (dynamic value, style) { + valueStyleBuilder: (node, dynamic value, style) { final isInt = int.tryParse(value.toString()); return PropertyOverrides( style: isInt != null