From 1e00fd0bdeaee0a06e2ae60505412dc8fde18e87 Mon Sep 17 00:00:00 2001 From: Abdullah Deshmukh Date: Mon, 16 Aug 2021 16:05:30 -0700 Subject: [PATCH] [linting_tool] Implement editing profile (#874) --- .../lib/model/editing_controller.dart | 51 +++++ .../lib/model/profiles_store.dart | 15 ++ .../linting_tool/lib/pages/rules_page.dart | 124 +++++++++-- .../lib/pages/saved_lints_page.dart | 17 +- .../lib/widgets/saved_rule_tile.dart | 199 ++++++++++-------- .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + experimental/linting_tool/macos/Podfile.lock | 6 + experimental/linting_tool/pubspec.lock | 49 +++++ experimental/linting_tool/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 366 insertions(+), 107 deletions(-) create mode 100644 experimental/linting_tool/lib/model/editing_controller.dart diff --git a/experimental/linting_tool/lib/model/editing_controller.dart b/experimental/linting_tool/lib/model/editing_controller.dart new file mode 100644 index 000000000..50e32abde --- /dev/null +++ b/experimental/linting_tool/lib/model/editing_controller.dart @@ -0,0 +1,51 @@ +// Copyright 2021 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:linting_tool/model/profile.dart'; +import 'package:linting_tool/model/profiles_store.dart'; +import 'package:linting_tool/model/rule.dart'; + +class EditingController extends ChangeNotifier { + bool _isEditing; + + EditingController({bool? isEditing}) : _isEditing = isEditing ?? false; + + bool get isEditing => _isEditing; + + set isEditing(bool enabled) { + _selectedRules.clear(); + _isEditing = enabled; + notifyListeners(); + } + + final List _selectedRules = []; + + List get selectedRules => _selectedRules; + + void selectRule(Rule rule) { + _selectedRules.add(rule); + notifyListeners(); + } + + void deselectRule(Rule rule) { + _selectedRules.remove(rule); + notifyListeners(); + } + + Future deleteSelected( + RulesProfile profile, ProfilesStore profilesStore) async { + var rules = profile.rules; + for (var rule in _selectedRules) { + rules.remove(rule); + } + + RulesProfile newProfile = RulesProfile(name: profile.name, rules: rules); + + await profilesStore.updateProfile(profile, newProfile); + + isEditing = false; + notifyListeners(); + } +} diff --git a/experimental/linting_tool/lib/model/profiles_store.dart b/experimental/linting_tool/lib/model/profiles_store.dart index cc00dc1d3..e6618d03a 100644 --- a/experimental/linting_tool/lib/model/profiles_store.dart +++ b/experimental/linting_tool/lib/model/profiles_store.dart @@ -76,6 +76,21 @@ class ProfilesStore extends ChangeNotifier { }); } + Future updateProfile( + RulesProfile oldProfile, RulesProfile newProfile) async { + await HiveService.updateBox(oldProfile, newProfile, _boxName); + + await Future.delayed(const Duration(milliseconds: 100), () async { + await fetchSavedProfiles(); + }); + } + + Future removeRuleFromProfile(RulesProfile profile, Rule rule) async { + var newProfile = + RulesProfile(name: profile.name, rules: profile.rules..remove(rule)); + await updateProfile(profile, newProfile); + } + Future deleteProfile(RulesProfile profile) async { await HiveService.deleteBox(profile, _boxName); diff --git a/experimental/linting_tool/lib/pages/rules_page.dart b/experimental/linting_tool/lib/pages/rules_page.dart index 0f9138678..7c3f6a8f9 100644 --- a/experimental/linting_tool/lib/pages/rules_page.dart +++ b/experimental/linting_tool/lib/pages/rules_page.dart @@ -2,16 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:context_menus/context_menus.dart'; import 'package:flutter/material.dart'; import 'package:linting_tool/layout/adaptive.dart'; -import 'package:linting_tool/model/profile.dart'; +import 'package:linting_tool/model/editing_controller.dart'; +import 'package:linting_tool/model/profiles_store.dart'; import 'package:linting_tool/widgets/saved_rule_tile.dart'; +import 'package:provider/provider.dart'; class RulesPage extends StatelessWidget { - final RulesProfile profile; + final int selectedProfileIndex; const RulesPage({ - required this.profile, + required this.selectedProfileIndex, Key? key, }) : super(key: key); @@ -33,7 +36,10 @@ class RulesPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text( - profile.name, + context + .read() + .savedProfiles[selectedProfileIndex] + .name, style: textTheme.subtitle2!.copyWith( color: textTheme.bodyText1!.color, ), @@ -53,21 +59,103 @@ class RulesPage extends StatelessWidget { backgroundColor: Colors.white, brightness: Brightness.light, ), - body: ListView.separated( - padding: EdgeInsetsDirectional.only( - start: startPadding, - end: endPadding, - top: isDesktop ? 28 : 0, - bottom: isDesktop ? kToolbarHeight : 0, + body: ContextMenuOverlay( + child: Consumer( + builder: (context, profilesStore, child) { + var profile = profilesStore.savedProfiles[selectedProfileIndex]; + return profile.rules.isEmpty + ? const Center( + child: Text('There are no rules added to the profile.'), + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.separated( + padding: EdgeInsetsDirectional.only( + start: startPadding, + end: endPadding, + top: isDesktop ? 28 : 0, + bottom: isDesktop ? kToolbarHeight : 0, + ), + itemCount: profile.rules.length, + cacheExtent: 5, + itemBuilder: (context, index) { + return ContextMenuRegion( + contextMenu: GenericContextMenu( + buttonConfigs: [ + ContextMenuButtonConfig( + 'Remove from profile', + onPressed: () { + context + .read() + .removeRuleFromProfile( + profile, profile.rules[index]); + }, + ), + ], + ), + child: SavedRuleTile( + rule: profile.rules[index], + ), + ); + }, + separatorBuilder: (context, index) => + const SizedBox(height: 4), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(top: 28), + child: Row( + children: [ + Consumer( + builder: (context, editingController, child) { + var isEditing = editingController.isEditing; + return isEditing + ? Column( + children: [ + IconButton( + icon: const Icon(Icons.done), + onPressed: () { + editingController.isEditing = + false; + }, + ), + if (editingController + .selectedRules.isNotEmpty) + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + editingController + .deleteSelected( + profile, + profilesStore, + ); + }, + ), + ], + ) + : IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + editingController.isEditing = true; + }, + ); + }, + ), + SizedBox( + width: isTablet + ? 30 + : isDesktop + ? 60 + : 16), + ], + ), + ), + ], + ); + }, ), - itemCount: profile.rules.length, - cacheExtent: 5, - itemBuilder: (context, index) { - return SavedRuleTile( - rule: profile.rules[index], - ); - }, - separatorBuilder: (context, index) => const SizedBox(height: 4), ), ); } diff --git a/experimental/linting_tool/lib/pages/saved_lints_page.dart b/experimental/linting_tool/lib/pages/saved_lints_page.dart index 0adb50fa0..7b1b06a1f 100644 --- a/experimental/linting_tool/lib/pages/saved_lints_page.dart +++ b/experimental/linting_tool/lib/pages/saved_lints_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:linting_tool/layout/adaptive.dart'; +import 'package:linting_tool/model/editing_controller.dart'; import 'package:linting_tool/model/profiles_store.dart'; import 'package:linting_tool/pages/rules_page.dart'; import 'package:linting_tool/theme/colors.dart'; @@ -56,7 +57,10 @@ class SavedLintsPage extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => RulesPage(profile: profile), + builder: (context) => ChangeNotifierProvider( + create: (context) => EditingController(), + child: RulesPage(selectedProfileIndex: index), + ), ), ); }, @@ -66,7 +70,16 @@ class SavedLintsPage extends StatelessWidget { IconButton( icon: const Icon(Icons.edit), onPressed: () { - // TODO(abd99): Implement edit functionality. + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChangeNotifierProvider( + create: (context) => + EditingController(isEditing: true), + child: RulesPage(selectedProfileIndex: index), + ), + ), + ); }, ), const SizedBox( diff --git a/experimental/linting_tool/lib/widgets/saved_rule_tile.dart b/experimental/linting_tool/lib/widgets/saved_rule_tile.dart index b8e333d0a..fe97cb927 100644 --- a/experimental/linting_tool/lib/widgets/saved_rule_tile.dart +++ b/experimental/linting_tool/lib/widgets/saved_rule_tile.dart @@ -4,9 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:linting_tool/model/editing_controller.dart'; import 'package:linting_tool/model/rule.dart'; import 'package:linting_tool/theme/app_theme.dart'; import 'package:linting_tool/theme/colors.dart'; +import 'package:provider/provider.dart'; class SavedRuleTile extends StatefulWidget { final Rule rule; @@ -21,6 +23,8 @@ class SavedRuleTile extends StatefulWidget { class _SavedRuleTileState extends State { var isExpanded = false; + var isSelected = false; + @override Widget build(BuildContext context) { var theme = Theme.of(context); @@ -30,103 +34,124 @@ class _SavedRuleTileState extends State { rule.incompatible.isNotEmpty ? rule.incompatible.join(', ') : 'none'; final setsString = rule.sets.isNotEmpty ? rule.sets.join(', ') : 'none'; - // TODO(abd99): Add option to remove rule from profile. - // TODO(abd99): Add right click functionality. - return ExpansionTile( - collapsedBackgroundColor: AppColors.white50, - title: Text( - rule.name, - style: textTheme.subtitle1!.copyWith( - fontWeight: FontWeight.w700, - ), - ), - subtitle: Text( - rule.description, - style: textTheme.caption!, - ), - initiallyExpanded: isExpanded, - onExpansionChanged: (value) { - setState(() { - isExpanded = value; - }); - }, - expandedAlignment: Alignment.centerLeft, - childrenPadding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - backgroundColor: AppColors.white50, - maintainState: true, - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'Group:', - style: textTheme.subtitle2, - ), - TextSpan( - text: ' ${rule.group}', - ), - ], + return Consumer( + builder: (context, editingController, child) { + return ExpansionTile( + collapsedBackgroundColor: AppColors.white50, + leading: editingController.isEditing + ? Checkbox( + value: isSelected && + editingController.selectedRules.contains(rule), + onChanged: (value) { + if (value!) { + editingController.selectRule(rule); + setState(() { + isSelected = value; + }); + } else { + editingController.deselectRule(rule); + setState(() { + isSelected = value; + }); + } + }, + ) + : null, + title: Text( + rule.name, + style: textTheme.subtitle1!.copyWith( + fontWeight: FontWeight.w700, + ), ), - textAlign: TextAlign.left, - ), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'Maturity:', - style: textTheme.subtitle2, - ), - TextSpan( - text: ' ${rule.maturity}', - ), - ], + subtitle: Text( + rule.description, + style: textTheme.caption!, + ), + initiallyExpanded: isExpanded, + onExpansionChanged: (value) { + setState(() { + isExpanded = value; + }); + }, + expandedAlignment: Alignment.centerLeft, + childrenPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, ), - textAlign: TextAlign.left, - ), - Text.rich( - TextSpan( - children: [ + backgroundColor: AppColors.white50, + maintainState: true, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( TextSpan( - text: 'Incompatible:', - style: textTheme.subtitle2, + children: [ + TextSpan( + text: 'Group:', + style: textTheme.subtitle2, + ), + TextSpan( + text: ' ${rule.group}', + ), + ], ), + textAlign: TextAlign.left, + ), + Text.rich( TextSpan( - text: ' $incompatibleString', + children: [ + TextSpan( + text: 'Maturity:', + style: textTheme.subtitle2, + ), + TextSpan( + text: ' ${rule.maturity}', + ), + ], ), - ], - ), - textAlign: TextAlign.left, - ), - Text.rich( - TextSpan( - children: [ + textAlign: TextAlign.left, + ), + Text.rich( TextSpan( - text: 'Sets:', - style: textTheme.subtitle2, + children: [ + TextSpan( + text: 'Incompatible:', + style: textTheme.subtitle2, + ), + TextSpan( + text: ' $incompatibleString', + ), + ], ), + textAlign: TextAlign.left, + ), + Text.rich( TextSpan( - text: ' $setsString', + children: [ + TextSpan( + text: 'Sets:', + style: textTheme.subtitle2, + ), + TextSpan( + text: ' $setsString', + ), + ], ), - ], - ), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 16.0, - ), - MarkdownBody( - data: rule.details, - selectable: true, - styleSheet: AppTheme.buildMarkDownTheme(theme), - ), - const SizedBox( - height: 16.0, - ), - ], + textAlign: TextAlign.left, + ), + const SizedBox( + height: 16.0, + ), + MarkdownBody( + data: rule.details, + selectable: true, + styleSheet: AppTheme.buildMarkDownTheme(theme), + ), + const SizedBox( + height: 16.0, + ), + ], + ); + }, ); } } diff --git a/experimental/linting_tool/linux/flutter/generated_plugin_registrant.cc b/experimental/linting_tool/linux/flutter/generated_plugin_registrant.cc index db1fd636a..2609641f6 100644 --- a/experimental/linting_tool/linux/flutter/generated_plugin_registrant.cc +++ b/experimental/linting_tool/linux/flutter/generated_plugin_registrant.cc @@ -5,9 +5,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/experimental/linting_tool/linux/flutter/generated_plugins.cmake b/experimental/linting_tool/linux/flutter/generated_plugins.cmake index 68205a330..627fdc7f8 100644 --- a/experimental/linting_tool/linux/flutter/generated_plugins.cmake +++ b/experimental/linting_tool/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + url_launcher_linux ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift b/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift index b700dcdae..021fe488e 100644 --- a/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,10 @@ import Foundation import file_selector_macos import path_provider_macos +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/experimental/linting_tool/macos/Podfile.lock b/experimental/linting_tool/macos/Podfile.lock index ba70f1766..46af999c9 100644 --- a/experimental/linting_tool/macos/Podfile.lock +++ b/experimental/linting_tool/macos/Podfile.lock @@ -4,11 +4,14 @@ PODS: - FlutterMacOS (1.0.0) - path_provider_macos (0.0.1): - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS DEPENDENCIES: - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: file_selector_macos: @@ -17,11 +20,14 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral path_provider_macos: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: file_selector_macos: ff6dc948d4ddd34e8602a1f60b7d0b4cc6051a47 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b + url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4 PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c diff --git a/experimental/linting_tool/pubspec.lock b/experimental/linting_tool/pubspec.lock index c47530063..c173b9015 100644 --- a/experimental/linting_tool/pubspec.lock +++ b/experimental/linting_tool/pubspec.lock @@ -148,6 +148,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + context_menus: + dependency: "direct main" + description: + name: context_menus + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0+5" convert: dependency: transitive description: @@ -651,6 +658,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.9" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" vector_math: dependency: transitive description: diff --git a/experimental/linting_tool/pubspec.yaml b/experimental/linting_tool/pubspec.yaml index 915c66eed..403fc2615 100644 --- a/experimental/linting_tool/pubspec.yaml +++ b/experimental/linting_tool/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: mockito: ^5.0.13 provider: ^5.0.0 yaml: ^3.1.0 + context_menus: ^0.1.0+5 dev_dependencies: flutter_test: diff --git a/experimental/linting_tool/windows/flutter/generated_plugin_registrant.cc b/experimental/linting_tool/windows/flutter/generated_plugin_registrant.cc index 0cb14e795..7133e517d 100644 --- a/experimental/linting_tool/windows/flutter/generated_plugin_registrant.cc +++ b/experimental/linting_tool/windows/flutter/generated_plugin_registrant.cc @@ -5,8 +5,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorPlugin")); + UrlLauncherPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherPlugin")); } diff --git a/experimental/linting_tool/windows/flutter/generated_plugins.cmake b/experimental/linting_tool/windows/flutter/generated_plugins.cmake index 63eda9b7b..b944b21a8 100644 --- a/experimental/linting_tool/windows/flutter/generated_plugins.cmake +++ b/experimental/linting_tool/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + url_launcher_windows ) set(PLUGIN_BUNDLED_LIBRARIES)