From 76999c6a463986920bdb34f79dd1d100243f7bc0 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 11 Jul 2022 17:31:22 +0800 Subject: [PATCH 001/121] feat: add flowy_editor package --- .../packages/flowy_editor/.gitignore | 30 ++++++++++ .../app_flowy/packages/flowy_editor/.metadata | 10 ++++ .../packages/flowy_editor/CHANGELOG.md | 3 + .../app_flowy/packages/flowy_editor/LICENSE | 1 + .../app_flowy/packages/flowy_editor/README.md | 39 +++++++++++++ .../flowy_editor/analysis_options.yaml | 4 ++ .../flowy_editor/assets/document.json | 46 ++++++++++++++++ .../flowy_editor/lib/document/node.dart | 55 +++++++++++++++++++ .../flowy_editor/lib/document/path.dart | 1 + .../flowy_editor/lib/document/state_tree.dart | 15 +++++ .../flowy_editor/lib/flowy_editor.dart | 9 +++ .../packages/flowy_editor/pubspec.yaml | 55 +++++++++++++++++++ .../flowy_editor/test/flowy_editor_test.dart | 18 ++++++ .../packages/flowy_infra/pubspec.lock | 16 +++--- .../flutter/generated_plugin_registrant.cc | 4 +- .../linux/flutter/generated_plugins.cmake | 8 +++ .../flowy_infra_ui/example/pubspec.lock | 23 +++++--- .../windows/flutter/generated_plugins.cmake | 8 +++ .../pubspec.lock | 38 ++++++------- .../flowy_infra_ui_web/pubspec.lock | 40 +++++++------- .../packages/flowy_infra_ui/pubspec.lock | 23 +++++--- .../packages/flowy_sdk/example/pubspec.lock | 20 +++---- .../windows/flutter/generated_plugins.cmake | 8 +++ .../app_flowy/packages/flowy_sdk/pubspec.lock | 16 +++--- 24 files changed, 407 insertions(+), 83 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/.gitignore create mode 100644 frontend/app_flowy/packages/flowy_editor/.metadata create mode 100644 frontend/app_flowy/packages/flowy_editor/CHANGELOG.md create mode 100644 frontend/app_flowy/packages/flowy_editor/LICENSE create mode 100644 frontend/app_flowy/packages/flowy_editor/README.md create mode 100644 frontend/app_flowy/packages/flowy_editor/analysis_options.yaml create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/document.json create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/node.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/path.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/pubspec.yaml create mode 100644 frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/.gitignore b/frontend/app_flowy/packages/flowy_editor/.gitignore new file mode 100644 index 0000000000..96486fd930 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/frontend/app_flowy/packages/flowy_editor/.metadata b/frontend/app_flowy/packages/flowy_editor/.metadata new file mode 100644 index 0000000000..d3da16e67e --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/.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: cd41fdd495f6944ecd3506c21e94c6567b073278 + channel: stable + +project_type: package diff --git a/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md b/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md new file mode 100644 index 0000000000..41cc7d8192 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/frontend/app_flowy/packages/flowy_editor/LICENSE b/frontend/app_flowy/packages/flowy_editor/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/frontend/app_flowy/packages/flowy_editor/README.md b/frontend/app_flowy/packages/flowy_editor/README.md new file mode 100644 index 0000000000..8b55e735b5 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/frontend/app_flowy/packages/flowy_editor/analysis_options.yaml b/frontend/app_flowy/packages/flowy_editor/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json new file mode 100644 index 0000000000..715b6c4e64 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json @@ -0,0 +1,46 @@ +{ + "document": { + "type": "root", + "children": [ + { + "type": "text", + "attributes": { + "text-type": "heading1" + } + }, + { + "type": "text", + "attributes": { + "tag": "*" + }, + "children": [ + { + "type": "text", + "attributes": { + "text-type": "check-box", + "check": true + } + }, + { + "type": "text", + "attributes": { + "tag": "**" + } + } + ] + }, + { + "type": "image", + "attributes": { + "url": "x.png" + } + }, + { + "type": "video", + "attributes": { + "url": "x.mp4" + } + } + ] + } +} \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart new file mode 100644 index 0000000000..095ef87980 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -0,0 +1,55 @@ +import 'dart:collection'; + +class Node extends LinkedListEntry { + Node? parent; + final String type; + final LinkedList children; + final Map attributes; + + Node({ + required this.type, + required this.children, + required this.attributes, + this.parent, + }); + + factory Node.fromJson(Map json) { + assert(json['type'] is String); + + final jType = json['type'] as String; + final jChildren = json['children'] as List?; + final jAttributes = json['attributes'] != null + ? Map.from(json['attributes'] as Map) + : {}; + + final LinkedList children = LinkedList(); + if (jChildren != null) { + children.addAll( + jChildren.map( + (jnode) => Node.fromJson( + Map.from(jnode), + ), + ), + ); + } + + return Node( + type: jType, + children: children, + attributes: jAttributes, + ); + } + + Map toJson() { + var map = { + 'type': type, + }; + if (children.isNotEmpty) { + map['children'] = children.map((node) => node.toJson()); + } + if (attributes.isNotEmpty) { + map['attributes'] = attributes; + } + return map; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart new file mode 100644 index 0000000000..35ac1f467e --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart @@ -0,0 +1 @@ +typedef Path = List; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart new file mode 100644 index 0000000000..3c347753c4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -0,0 +1,15 @@ +import 'package:flowy_editor/document/node.dart'; + +class StateTree { + Node root; + + StateTree({required this.root}); + + factory StateTree.fromJson(Map json) { + assert(json['document'] is Map); + + final document = Map.from(json['document'] as Map); + final root = Node.fromJson(document); + return StateTree(root: root); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart new file mode 100644 index 0000000000..4a71c0211d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -0,0 +1,9 @@ +library flowy_editor; + +import 'package:flowy_editor/document/state_tree.dart'; + +class Example { + StateTree createStateTree(Map json) { + return StateTree.fromJson(json); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml new file mode 100644 index 0000000000..a8c4f0c430 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -0,0 +1,55 @@ +name: flowy_editor +description: A new Flutter package project. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.17.3 <3.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + assets: + - document.json + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart new file mode 100644 index 0000000000..b1b6dddb39 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; + +import 'package:flowy_editor/document/state_tree.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('create state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + expect(stateTree.root.type, 'root'); + expect(stateTree.root.toJson(), data['document']); + expect(stateTree.root.children.last.type, 'video'); + }); +} diff --git a/frontend/app_flowy/packages/flowy_infra/pubspec.lock b/frontend/app_flowy/packages/flowy_infra/pubspec.lock index 256c2dac3a..fc492e7344 100644 --- a/frontend/app_flowy/packages/flowy_infra/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_infra/pubspec.lock @@ -42,7 +42,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" crypto: dependency: transitive description: @@ -56,7 +56,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -101,7 +101,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -115,7 +115,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -148,7 +148,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -183,7 +183,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" textstyle_extensions: dependency: "direct main" description: @@ -218,7 +218,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" xml: dependency: transitive description: @@ -227,5 +227,5 @@ packages: source: hosted version: "5.2.0" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.24.0-7.0" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugin_registrant.cc index d0195b3a44..28d352c9d0 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,10 @@ #include "generated_plugin_registrant.h" -#include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flowy_infra_ui_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlowyInfraUIPlugin"); - flowy_infra_ui_plugin_register_with_registrar(flowy_infra_ui_registrar); + flowy_infra_u_i_plugin_register_with_registrar(flowy_infra_ui_registrar); } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugins.cmake index 8ba3e19a28..98453e694d 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST flowy_infra_ui ) +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 $) 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}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock index 0a91c0b58c..eaaa5db680 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock @@ -49,7 +49,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" crypto: dependency: transitive description: @@ -84,7 +84,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flowy_infra: dependency: transitive description: @@ -148,7 +148,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" lint: dependency: transitive description: @@ -177,6 +177,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: @@ -197,7 +204,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -244,7 +251,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -286,7 +293,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.9" textstyle_extensions: dependency: transitive description: @@ -321,7 +328,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" xml: dependency: transitive description: @@ -330,5 +337,5 @@ packages: source: hosted version: "5.3.1" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=2.0.0" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_infra_ui/example/windows/flutter/generated_plugins.cmake index c82a15ca3f..8571d27085 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/windows/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST flowy_infra_ui ) +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 $) 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/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock index a2ed8a8fe8..7309cce94a 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,14 +21,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -42,14 +42,14 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -80,21 +80,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" plugin_platform_interface: dependency: "direct main" description: @@ -113,7 +120,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -148,21 +155,14 @@ packages: 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" + version: "0.4.9" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.17.0" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock index 804b67b5b9..bdc4a1ae65 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,14 +21,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -42,14 +42,14 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flowy_infra_ui_platform_interface: dependency: "direct main" description: @@ -85,7 +85,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" lints: dependency: transitive description: @@ -99,21 +99,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" plugin_platform_interface: dependency: transitive description: @@ -132,7 +139,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -167,21 +174,14 @@ packages: 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" + version: "0.4.9" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.17.0" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock index 8ff7b9870b..c24e86d22d 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock @@ -49,7 +49,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" crypto: dependency: transitive description: @@ -77,7 +77,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flowy_infra: dependency: "direct main" description: @@ -134,7 +134,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" lint: dependency: transitive description: @@ -163,6 +163,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: @@ -183,7 +190,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -230,7 +237,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -272,7 +279,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.9" textstyle_extensions: dependency: "direct main" description: @@ -307,7 +314,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" xml: dependency: transitive description: @@ -316,5 +323,5 @@ packages: source: hosted version: "5.3.1" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=2.0.0" diff --git a/frontend/app_flowy/packages/flowy_sdk/example/pubspec.lock b/frontend/app_flowy/packages/flowy_sdk/example/pubspec.lock index 921bd4fb2b..ef6b2235e4 100644 --- a/frontend/app_flowy/packages/flowy_sdk/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_sdk/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.6" + version: "3.1.11" async: dependency: transitive description: @@ -49,7 +49,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" crypto: dependency: transitive description: @@ -77,7 +77,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: transitive description: @@ -186,7 +186,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -200,7 +200,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" platform: dependency: transitive description: @@ -233,7 +233,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -275,7 +275,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" typed_data: dependency: transitive description: @@ -289,14 +289,14 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" vm_service: dependency: transitive description: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "7.5.0" + version: "8.2.2" webdriver: dependency: transitive description: @@ -305,5 +305,5 @@ packages: source: hosted version: "3.0.0" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.17.0" diff --git a/frontend/app_flowy/packages/flowy_sdk/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_sdk/example/windows/flutter/generated_plugins.cmake index d71dbaabab..33c0fd0169 100644 --- a/frontend/app_flowy/packages/flowy_sdk/example/windows/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/flowy_sdk/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST flowy_sdk ) +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 $) 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/frontend/app_flowy/packages/flowy_sdk/pubspec.lock b/frontend/app_flowy/packages/flowy_sdk/pubspec.lock index 1f5af870d9..0539950c64 100644 --- a/frontend/app_flowy/packages/flowy_sdk/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_sdk/pubspec.lock @@ -140,7 +140,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: @@ -175,7 +175,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: "direct main" description: @@ -318,7 +318,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -346,7 +346,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" pedantic: dependency: transitive description: @@ -414,7 +414,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -456,7 +456,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" timing: dependency: transitive description: @@ -477,7 +477,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" watcher: dependency: transitive description: @@ -500,5 +500,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.17.0" From 47436bf6e249190d5153171f77f10466d5c34fc7 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 11 Jul 2022 18:38:08 +0800 Subject: [PATCH 002/121] feat: find node with path or index in state tree --- .../flowy_editor/lib/document/node.dart | 18 ++++++++++++++++++ .../flowy_editor/lib/document/state_tree.dart | 5 +++++ .../flowy_editor/test/flowy_editor_test.dart | 5 +++++ 3 files changed, 28 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 095ef87980..deef71b7ec 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,5 +1,7 @@ import 'dart:collection'; +import 'package:flowy_editor/document/path.dart'; + class Node extends LinkedListEntry { Node? parent; final String type; @@ -40,6 +42,22 @@ class Node extends LinkedListEntry { ); } + Node? childAtIndex(int index) { + if (children.length <= index) { + return null; + } + + return children.elementAt(index); + } + + Node? childAtPath(Path path) { + if (path.isEmpty) { + return this; + } + + return childAtIndex(path.first)?.childAtPath(path.sublist(1)); + } + Map toJson() { var map = { 'type': type, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 3c347753c4..82ee25976c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -12,4 +12,9 @@ class StateTree { final root = Node.fromJson(document); return StateTree(root: root); } + + // bool insert(Path path, Node node) { + // final insertedNode = root + // return false; + // } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index b1b6dddb39..0c9dcfee0c 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -14,5 +14,10 @@ void main() { expect(stateTree.root.type, 'root'); expect(stateTree.root.toJson(), data['document']); expect(stateTree.root.children.last.type, 'video'); + + final checkBoxNode = stateTree.root.childAtPath([1, 0]); + expect(checkBoxNode != null, true); + final textType = checkBoxNode!.attributes['text-type']; + expect(textType != null, true); }); } From 59d92a8ced7bcd7a6346e00d32736de35f8030f3 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 11 Jul 2022 20:37:12 +0800 Subject: [PATCH 003/121] feat: insert / delelte / update / search node in state tree --- .../flowy_editor/assets/document.json | 1 + .../flowy_editor/lib/document/node.dart | 33 ++++++++++++- .../flowy_editor/lib/document/state_tree.dart | 46 +++++++++++++++++-- .../flowy_editor/test/flowy_editor_test.dart | 40 +++++++++++++++- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json index 715b6c4e64..5c9ca5175b 100644 --- a/frontend/app_flowy/packages/flowy_editor/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json @@ -40,6 +40,7 @@ "attributes": { "url": "x.mp4" } + } ] } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index deef71b7ec..743ae6fd15 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,5 +1,4 @@ import 'dart:collection'; - import 'package:flowy_editor/document/path.dart'; class Node extends LinkedListEntry { @@ -35,11 +34,23 @@ class Node extends LinkedListEntry { ); } - return Node( + final node = Node( type: jType, children: children, attributes: jAttributes, ); + + for (final child in children) { + child.parent = node; + } + + return node; + } + + void updateAttributes(Map attributes) { + for (final attribute in attributes.entries) { + this.attributes[attribute.key] = attribute.value; + } } Node? childAtIndex(int index) { @@ -58,6 +69,24 @@ class Node extends LinkedListEntry { return childAtIndex(path.first)?.childAtPath(path.sublist(1)); } + @override + void insertAfter(Node entry) { + entry.parent = parent; + super.insertAfter(entry); + } + + @override + void insertBefore(Node entry) { + entry.parent = parent; + super.insertBefore(entry); + } + + @override + void unlink() { + parent = null; + super.unlink(); + } + Map toJson() { var map = { 'type': type, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 82ee25976c..57b601c0e2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -1,7 +1,8 @@ import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/path.dart'; class StateTree { - Node root; + final Node root; StateTree({required this.root}); @@ -13,8 +14,43 @@ class StateTree { return StateTree(root: root); } - // bool insert(Path path, Node node) { - // final insertedNode = root - // return false; - // } + Node? nodeAtPath(Path path) { + return root.childAtPath(path); + } + + bool insert(Path path, Node node) { + if (path.isEmpty) { + return false; + } + final insertedNode = root.childAtPath( + path.sublist(0, path.length - 1) + [path.last - 1], + ); + if (insertedNode == null) { + return false; + } + insertedNode.insertAfter(node); + return true; + } + + Node? delete(Path path) { + if (path.isEmpty) { + return null; + } + final deletedNode = root.childAtPath(path); + deletedNode?.unlink(); + return deletedNode; + } + + Map? update(Path path, Map attributes) { + if (path.isEmpty) { + return null; + } + final updatedNode = root.childAtPath(path); + if (updatedNode == null) { + return null; + } + final previousAttributes = {...updatedNode.attributes}; + updatedNode.updateAttributes(attributes); + return previousAttributes; + } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index 0c9dcfee0c..8e3c93b865 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/state_tree.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,11 +14,48 @@ void main() { final stateTree = StateTree.fromJson(data); expect(stateTree.root.type, 'root'); expect(stateTree.root.toJson(), data['document']); - expect(stateTree.root.children.last.type, 'video'); + }); + test('search node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); final checkBoxNode = stateTree.root.childAtPath([1, 0]); expect(checkBoxNode != null, true); final textType = checkBoxNode!.attributes['text-type']; expect(textType != null, true); }); + + test('insert node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final insertNode = Node.fromJson({ + 'type': 'text', + }); + bool result = stateTree.insert([1, 1], insertNode); + expect(result, true); + expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true); + }); + + test('delete node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final deletedNode = stateTree.delete([1, 0]); + expect(deletedNode != null, true); + expect(deletedNode!.attributes['text-type'], 'check-box'); + }); + + test('update node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final attributes = stateTree.update([1, 0], {'text-type': 'heading1'}); + expect(attributes != null, true); + expect(attributes!['text-type'], 'check-box'); + final updatedNode = stateTree.nodeAtPath([1, 0]); + expect(updatedNode != null, true); + expect(updatedNode!.attributes['text-type'], 'heading1'); + }); } From d2e62f882b8bfb51f329c796b0d2a089791186fd Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 11 Jul 2022 21:05:43 +0800 Subject: [PATCH 004/121] feat: typedef Map to Attributes --- .../packages/flowy_editor/assets/document.json | 1 - .../packages/flowy_editor/lib/document/node.dart | 14 ++++++++------ .../flowy_editor/lib/document/state_tree.dart | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json index 5c9ca5175b..715b6c4e64 100644 --- a/frontend/app_flowy/packages/flowy_editor/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json @@ -40,7 +40,6 @@ "attributes": { "url": "x.mp4" } - } ] } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 743ae6fd15..e4ff84b99c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,11 +1,13 @@ import 'dart:collection'; import 'package:flowy_editor/document/path.dart'; +typedef Attributes = Map; + class Node extends LinkedListEntry { Node? parent; final String type; final LinkedList children; - final Map attributes; + final Attributes attributes; Node({ required this.type, @@ -20,15 +22,15 @@ class Node extends LinkedListEntry { final jType = json['type'] as String; final jChildren = json['children'] as List?; final jAttributes = json['attributes'] != null - ? Map.from(json['attributes'] as Map) - : {}; + ? Attributes.from(json['attributes'] as Map) + : Attributes.from({}); final LinkedList children = LinkedList(); if (jChildren != null) { children.addAll( jChildren.map( - (jnode) => Node.fromJson( - Map.from(jnode), + (jChild) => Node.fromJson( + Map.from(jChild), ), ), ); @@ -47,7 +49,7 @@ class Node extends LinkedListEntry { return node; } - void updateAttributes(Map attributes) { + void updateAttributes(Attributes attributes) { for (final attribute in attributes.entries) { this.attributes[attribute.key] = attribute.value; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 57b601c0e2..368b575c90 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -6,7 +6,7 @@ class StateTree { StateTree({required this.root}); - factory StateTree.fromJson(Map json) { + factory StateTree.fromJson(Attributes json) { assert(json['document'] is Map); final document = Map.from(json['document'] as Map); @@ -41,7 +41,7 @@ class StateTree { return deletedNode; } - Map? update(Path path, Map attributes) { + Attributes? update(Path path, Attributes attributes) { if (path.isEmpty) { return null; } From 9e4227d3d2b36f6f072c2002b7c9d991b8911de9 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 11 Jul 2022 21:14:32 +0800 Subject: [PATCH 005/121] test: add delete node test --- .../app_flowy/packages/flowy_editor/assets/document.json | 7 +++++++ .../packages/flowy_editor/test/flowy_editor_test.dart | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json index 715b6c4e64..8aa75717ac 100644 --- a/frontend/app_flowy/packages/flowy_editor/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json @@ -14,6 +14,13 @@ "tag": "*" }, "children": [ + { + "type": "text", + "attributes": { + "text-type": "heading2", + "check": true + } + }, { "type": "text", "attributes": { diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index 8e3c93b865..e8f14bc9c7 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -42,19 +42,22 @@ void main() { final String response = await rootBundle.loadString('assets/document.json'); final data = Map.from(json.decode(response)); final stateTree = StateTree.fromJson(data); - final deletedNode = stateTree.delete([1, 0]); + final deletedNode = stateTree.delete([1, 1]); expect(deletedNode != null, true); expect(deletedNode!.attributes['text-type'], 'check-box'); + final node = stateTree.nodeAtPath([1, 1]); + expect(node != null, true); + expect(node!.attributes['tag'], '**'); }); test('update node in state tree', () async { final String response = await rootBundle.loadString('assets/document.json'); final data = Map.from(json.decode(response)); final stateTree = StateTree.fromJson(data); - final attributes = stateTree.update([1, 0], {'text-type': 'heading1'}); + final attributes = stateTree.update([1, 1], {'text-type': 'heading1'}); expect(attributes != null, true); expect(attributes!['text-type'], 'check-box'); - final updatedNode = stateTree.nodeAtPath([1, 0]); + final updatedNode = stateTree.nodeAtPath([1, 1]); expect(updatedNode != null, true); expect(updatedNode!.attributes['text-type'], 'heading1'); }); From 9c73a8cd9ab86e931d796a1df31c3d7ae79bfaa7 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 12 Jul 2022 11:54:06 +0800 Subject: [PATCH 006/121] feat: position and selection --- .../flowy_editor/lib/document/path.dart | 6 ++ .../flowy_editor/lib/document/position.dart | 28 ++++++++ .../flowy_editor/lib/document/selection.dart | 28 ++++++++ .../flowy_editor/test/flowy_editor_test.dart | 69 +++++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/position.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart index 35ac1f467e..a8163f094d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart @@ -1 +1,7 @@ +import 'package:flutter/foundation.dart'; + typedef Path = List; + +bool pathEquals(Path path1, Path path2) { + return listEquals(path1, path2); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart new file mode 100644 index 0000000000..2c7d85f908 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import './path.dart'; + +class Position { + final Path path; + final int offset; + + Position({ + required this.path, + this.offset = 0, + }); + + @override + bool operator==(Object other) { + if (other is! Position) { + return false; + } + return pathEquals(path, other.path) && offset == other.offset; + } + + @override + int get hashCode { + final pathHash = hashList(path); + return pathHash ^ offset; + } + +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart new file mode 100644 index 0000000000..a03ccae37f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -0,0 +1,28 @@ +import './position.dart'; + +class Selection { + final Position start; + final Position end; + + Selection({ + required this.start, + required this.end, + }); + + factory Selection.collapsed(Position pos) { + return Selection(start: pos, end: pos); + } + + Selection collapse({ bool atStart = false }) { + if (atStart) { + return Selection(start: start, end: start); + } else { + return Selection(start: end, end: end); + } + } + + bool isCollapsed() { + return start == end; + } + +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index e8f14bc9c7..cb67f61d96 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -2,6 +2,10 @@ import 'dart:convert'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/state_tree.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -61,4 +65,69 @@ void main() { expect(updatedNode != null, true); expect(updatedNode!.attributes['text-type'], 'heading1'); }); + + test('test path utils 1', () { + final path1 = [1]; + final path2 = [1]; + expect(pathEquals(path1, path2), true); + + expect(hashList(path1), hashList(path2)); + }); + + test('test path utils 2', () { + final path1 = [1]; + final path2 = [2]; + expect(pathEquals(path1, path2), false); + + expect(hashList(path1) != hashList(path2), true); + }); + + test('test position comparator', () { + final pos1 = Position(path: [1], offset: 0); + final pos2 = Position(path: [1], offset: 0); + expect(pos1 == pos2, true); + expect(pos1.hashCode == pos2.hashCode, true); + }); + + test('test position comparator with offset', () { + final pos1 = Position(path: [1, 1, 1, 1, 1], offset: 100); + final pos2 = Position(path: [1, 1, 1, 1, 1], offset: 100); + expect(pos1, pos2); + expect(pos1.hashCode, pos2.hashCode); + }); + + test('test position comparator false', () { + final pos1 = Position(path: [1, 1, 1, 1, 1], offset: 100); + final pos2 = Position(path: [1, 1, 2, 1, 1], offset: 100); + expect(pos1 == pos2, false); + expect(pos1.hashCode == pos2.hashCode, false); + }); + + test('test position comparator with offset false', () { + final pos1 = Position(path: [1, 1, 1, 1, 1], offset: 100); + final pos2 = Position(path: [1, 1, 1, 1, 1], offset: 101); + expect(pos1 == pos2, false); + expect(pos1.hashCode == pos2.hashCode, false); + }); + + test('test selection comparator', () { + final pos = Position(path: [0], offset: 0); + final sel = Selection.collapsed(pos); + expect(sel.start, sel.end); + expect(sel.isCollapsed(), true); + }); + + test('test selection collapse', () { + final start = Position(path: [0], offset: 0); + final end = Position(path: [0], offset: 10); + final sel = Selection(start: start, end: end); + + final collapsedSelAtStart = sel.collapse(atStart: true); + expect(collapsedSelAtStart.start, start); + expect(collapsedSelAtStart.end, start); + + final collapsedSelAtEnd = sel.collapse(); + expect(collapsedSelAtEnd.start, end); + expect(collapsedSelAtEnd.end, end); + }); } From 1b0c29ea09ac8b5a9e702bd332c18cba6feb7256 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 12 Jul 2022 13:34:40 +0800 Subject: [PATCH 007/121] feat: implement editor state operation --- .../flowy_editor/lib/editor_state.dart | 32 ++++++++++ .../flowy_editor/lib/operation/operation.dart | 58 +++++++++++++++++++ .../lib/operation/transaction.dart | 6 ++ 3 files changed, 96 insertions(+) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart new file mode 100644 index 0000000000..d0f8c39847 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -0,0 +1,32 @@ +import 'package:flowy_editor/operation/operation.dart'; + +import './document/state_tree.dart'; +import './document/selection.dart'; +import './operation/operation.dart'; +import './operation/transaction.dart'; + +class EditorState { + final StateTree document; + Selection? cursorSelection; + + EditorState({ + required this.document, + }); + + apply(Transaction transaction) { + for (final op in transaction.operations) { + _applyOperation(op); + } + } + + _applyOperation(Operation op) { + if (op is InsertOperation) { + document.insert(op.path, op.value); + } else if (op is UpdateOperation) { + document.update(op.path, op.attributes); + } else if (op is DeleteOperation) { + document.delete(op.path); + } + } + +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart new file mode 100644 index 0000000000..b5d71b57d4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -0,0 +1,58 @@ +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/node.dart'; + +abstract class Operation { + + Operation invert(); + +} + +class InsertOperation extends Operation { + final Path path; + final Node value; + + InsertOperation({ + required this.path, + required this.value, + }); + + @override + Operation invert() { + return DeleteOperation(path: path, removedValue: value); + } + +} + +class UpdateOperation extends Operation { + final Path path; + final Attributes attributes; + final Attributes oldAttributes; + + UpdateOperation({ + required this.path, + required this.attributes, + required this.oldAttributes, + }); + + @override + Operation invert() { + return UpdateOperation(path: path, attributes: oldAttributes, oldAttributes: attributes); + } + +} + +class DeleteOperation extends Operation { + final Path path; + final Node removedValue; + + DeleteOperation({ + required this.path, + required this.removedValue, + }); + + @override + Operation invert() { + return InsertOperation(path: path, value: removedValue); + } + +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart new file mode 100644 index 0000000000..c6fbed63aa --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart @@ -0,0 +1,6 @@ +import './operation.dart'; + +class Transaction { + final List operations = []; + +} From a298348def3d24fc1056f6b2ee04c46a55b4d28d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 12 Jul 2022 17:30:32 +0800 Subject: [PATCH 008/121] feat: add render plugins and example --- .../packages/flowy_editor/example/.gitignore | 47 ++ .../packages/flowy_editor/example/.metadata | 45 ++ .../packages/flowy_editor/example/README.md | 16 + .../example/analysis_options.yaml | 29 + .../flowy_editor/example/android/.gitignore | 13 + .../example/android/app/build.gradle | 71 +++ .../android/app/src/debug/AndroidManifest.xml | 8 + .../android/app/src/main/AndroidManifest.xml | 34 ++ .../com/example/example/MainActivity.kt | 6 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 8 + .../flowy_editor/example/android/build.gradle | 31 + .../example/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../example/android/settings.gradle | 11 + .../flowy_editor/example/assets/document.json | 34 ++ .../flowy_editor/example/ios/.gitignore | 34 ++ .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../example/ios/Flutter/Debug.xcconfig | 1 + .../example/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 484 +++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 ++ .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Info.plist | 49 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + .../flowy_editor/example/lib/main.dart | 93 +++ .../example/lib/plugin/text_node_widget.dart | 68 +++ .../flowy_editor/example/linux/.gitignore | 1 + .../flowy_editor/example/linux/CMakeLists.txt | 138 +++++ .../example/linux/flutter/CMakeLists.txt | 88 +++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 23 + .../flowy_editor/example/linux/main.cc | 6 + .../example/linux/my_application.cc | 104 ++++ .../example/linux/my_application.h | 18 + .../flowy_editor/example/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 10 + .../macos/Runner.xcodeproj/project.pbxproj | 572 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 +++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 46993 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 3276 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 1429 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 5933 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1243 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 14800 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 1874 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + .../example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../example/macos/Runner/Release.entitlements | 8 + .../flowy_editor/example/pubspec.lock | 175 ++++++ .../flowy_editor/example/pubspec.yaml | 91 +++ .../example/test/widget_test.dart | 30 + .../flowy_editor/example/web/favicon.png | Bin 0 -> 917 bytes .../example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../flowy_editor/example/web/index.html | 58 ++ .../flowy_editor/example/web/manifest.json | 35 ++ .../flowy_editor/example/windows/.gitignore | 17 + .../example/windows/CMakeLists.txt | 101 ++++ .../example/windows/flutter/CMakeLists.txt | 104 ++++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 23 + .../example/windows/runner/CMakeLists.txt | 32 + .../example/windows/runner/Runner.rc | 121 ++++ .../example/windows/runner/flutter_window.cpp | 61 ++ .../example/windows/runner/flutter_window.h | 33 + .../example/windows/runner/main.cpp | 43 ++ .../example/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 20 + .../example/windows/runner/utils.cpp | 64 ++ .../example/windows/runner/utils.h | 19 + .../example/windows/runner/win32_window.cpp | 245 ++++++++ .../example/windows/runner/win32_window.h | 98 +++ .../flowy_editor/lib/flowy_editor.dart | 12 +- .../lib/render/base_node_widget.dart | 24 + .../lib/render/render_plugins.dart | 35 ++ 131 files changed, 4637 insertions(+), 7 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/.gitignore create mode 100644 frontend/app_flowy/packages/flowy_editor/example/.metadata create mode 100644 frontend/app_flowy/packages/flowy_editor/example/README.md create mode 100644 frontend/app_flowy/packages/flowy_editor/example/analysis_options.yaml create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/.gitignore create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/build.gradle create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/build.gradle create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/gradle.properties create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 frontend/app_flowy/packages/flowy_editor/example/android/settings.gradle create mode 100644 frontend/app_flowy/packages/flowy_editor/example/assets/document.json create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/.gitignore create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/AppDelegate.swift create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/main.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/example/linux/.gitignore create mode 100644 frontend/app_flowy/packages/flowy_editor/example/linux/CMakeLists.txt create mode 100644 frontend/app_flowy/packages/flowy_editor/example/linux/flutter/CMakeLists.txt create mode 100644 frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc create mode 100644 frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.h create mode 100644 frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake create mode 100644 frontend/app_flowy/packages/flowy_editor/example/linux/main.cc create mode 100644 frontend/app_flowy/packages/flowy_editor/example/linux/my_application.cc create mode 100644 frontend/app_flowy/packages/flowy_editor/example/linux/my_application.h create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/.gitignore create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/AppDelegate.swift create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Release.xcconfig create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Info.plist create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/MainFlutterWindow.swift create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements create mode 100644 frontend/app_flowy/packages/flowy_editor/example/pubspec.lock create mode 100644 frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml create mode 100644 frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/example/web/favicon.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-192.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-512.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-192.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-512.png create mode 100644 frontend/app_flowy/packages/flowy_editor/example/web/index.html create mode 100644 frontend/app_flowy/packages/flowy_editor/example/web/manifest.json create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/.gitignore create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/Runner.rc create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/resource.h create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/resources/app_icon.ico create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.cpp create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.h create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.cpp create mode 100644 frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.h create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/base_node_widget.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/.gitignore new file mode 100644 index 0000000000..a8e938c083 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# 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/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/frontend/app_flowy/packages/flowy_editor/example/.metadata b/frontend/app_flowy/packages/flowy_editor/example/.metadata new file mode 100644 index 0000000000..ed0b5185fb --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/.metadata @@ -0,0 +1,45 @@ +# 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. + +version: + revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + - platform: android + create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + - platform: ios + create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + - platform: linux + create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + - platform: macos + create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + - platform: web + create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + - platform: windows + create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend/app_flowy/packages/flowy_editor/example/README.md b/frontend/app_flowy/packages/flowy_editor/example/README.md new file mode 100644 index 0000000000..2b3fce4c86 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/frontend/app_flowy/packages/flowy_editor/example/analysis_options.yaml b/frontend/app_flowy/packages/flowy_editor/example/analysis_options.yaml new file mode 100644 index 0000000000..61b6c4de17 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/android/.gitignore new file mode 100644 index 0000000000..6f568019d3 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/build.gradle b/frontend/app_flowy/packages/flowy_editor/example/android/app/build.gradle new file mode 100644 index 0000000000..0833ecfca8 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/build.gradle @@ -0,0 +1,71 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..45d523a2a2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..3f41384dbc --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 0000000000..e793a000d6 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable-v21/launch_background.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000..f74085f3f6 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable/launch_background.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000000..304732f884 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000..06952be745 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..cb1ef88056 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000000..45d523a2a2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/build.gradle b/frontend/app_flowy/packages/flowy_editor/example/android/build.gradle new file mode 100644 index 0000000000..83ae220041 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/gradle.properties b/frontend/app_flowy/packages/flowy_editor/example/android/gradle.properties new file mode 100644 index 0000000000..94adc3a3f9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/gradle/wrapper/gradle-wrapper.properties b/frontend/app_flowy/packages/flowy_editor/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..cc5527d781 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/settings.gradle b/frontend/app_flowy/packages/flowy_editor/example/android/settings.gradle new file mode 100644 index 0000000000..44e62bcf06 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json new file mode 100644 index 0000000000..4613a523aa --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -0,0 +1,34 @@ +{ + "document": { + "type": "text", + "attributes": { + "content": "TITLE" + }, + "children": [ + { + "type": "text", + "attributes": { + "text-type": "heading1", + "font-size": 30, + "content": "aaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "type": "text", + "attributes": { + "text-type": "heading1", + "font-size": 30, + "content": "bbbbbbbbbbbbbbbbbbbbbbb" + } + }, + { + "type": "text", + "attributes": { + "text-type": "heading1", + "font-size": 30, + "content": "cccccccccccccccccccccc" + } + } + ] + } + } \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/ios/.gitignore new file mode 100644 index 0000000000..7a7f9873ad --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/AppFrameworkInfo.plist b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000000..8d4492f977 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000000..592ceee85b --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000000..592ceee85b --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..813642b9a4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,484 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 446D3AAR7E; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 446D3AAR7E; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 446D3AAR7E; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..f9b0d7c5ea --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000..c87d15a335 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1d526a16ed --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..f9b0d7c5ea --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/AppDelegate.swift b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000000..70693e4a8c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..d36b1fab2d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 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 literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 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 literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000000..89c2725b70 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..f2e259c7c9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..f3c28516fb --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist new file mode 100644 index 0000000000..907f329fe0 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000000..308a2a560b --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart new file mode 100644 index 0000000000..d8ad964205 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:example/plugin/text_node_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/services.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // Try running your application with "flutter run". You'll see the + // application has a blue toolbar. Then, without quitting the app, try + // changing the primarySwatch below to Colors.green and then invoke + // "hot reload" (press "r" in the console where you ran "flutter run", + // or simply save your changes to "hot reload" in a Flutter IDE). + // Notice that the counter didn't reset back to zero; the application + // is not restarted. + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final RenderPlugins renderPlugins = RenderPlugins(); + @override + void initState() { + super.initState(); + + renderPlugins.register( + 'text', + textNodeWidgetBuilder, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: FutureBuilder( + future: rootBundle.loadString('assets/document.json'), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + final data = Map.from(json.decode(snapshot.data!)); + final stateTree = StateTree.fromJson(data); + return renderPlugins.buildWidgetWithNode( + stateTree.root, + ); + } + }, + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart new file mode 100644 index 0000000000..6b1617d7d8 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flowy_editor/flowy_editor.dart'; + +NodeWidgetBuilder textNodeWidgetBuilder = + (node, renderPlugins) => TextNodeWidget( + node: node, + renderPlugins: renderPlugins, + ); + +class TextNodeWidget extends BaseNodeWidget { + const TextNodeWidget({ + super.key, + required super.node, + required super.renderPlugins, + }); + + @override + State createState() => _TextNodeWidgetState(); +} + +class _TextNodeWidgetState extends State { + Node get node => widget.node; + + @override + Widget build(BuildContext context) { + final childWidget = renderChildren(); + final richText = RichText( + text: TextSpan( + text: node.attributes['content'] as String, + style: node.attributes.toTextStyle(), + ), + ); + if (childWidget != null) { + return Column( + children: [richText, childWidget], + ); + } else { + return richText; + } + } + + // manage children's render + Widget? renderChildren() { + if (node.children.isEmpty) { + return null; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => widget.renderPlugins.buildWidgetWithNode( + e, + ), + ) + .toList(), + ); + } +} + +extension on Attributes { + TextStyle toTextStyle() { + return TextStyle( + color: this['color'] != null ? Colors.red : Colors.black, + fontSize: this['font-size'] != null ? 30 : 15, + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/linux/.gitignore new file mode 100644 index 0000000000..d3896c9844 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/linux/CMakeLists.txt new file mode 100644 index 0000000000..74c66dd446 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000000..d5bd01648a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000000..e71a16d23d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.h b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000000..e0f0a47bc0 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000000..2e1de87a7e --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + 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}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/main.cc b/frontend/app_flowy/packages/flowy_editor/example/linux/main.cc new file mode 100644 index 0000000000..e7c5c54370 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.cc b/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.cc new file mode 100644 index 0000000000..0ba8f43096 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.h b/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.h new file mode 100644 index 0000000000..72271d5e41 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/macos/.gitignore new file mode 100644 index 0000000000..746adbb6b9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000000..c2efd0b608 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000000..c2efd0b608 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000000..cccf817a52 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..c84862c675 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000..fb7259e177 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1d526a16ed --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/AppDelegate.swift b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000000..d53ef64377 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..a2ec33f19f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..3c4935a7ca84f0976aca34b7f2895d65fb94d1ea GIT binary patch literal 46993 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 literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4cc16421680a50164ba74381b4b35ceaa0ccfc GIT binary patch literal 3276 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 N;OXk;vd$@?2>?>Ex^Vyi literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..bcbf36df2f2aaaa0a63c7dabc94e600184229d0d GIT binary patch literal 5933 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 f$KZHO*s&vt|9^w-6P?|#0pRK8NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?!xdN1Q+aGJ{c&& zS>O>_%)r1c48n{Iv*t(u1=&kHeO=ifbFy+6aSK)V_AxLppYn8Z42d|rc6w}vOsL55 z`t&mC&y2@JTEyg!eDiFX^k#CC!jq%>erB=yHqUP0XcDOTw6ko}L zX;EmMrq(fKk*eygEuA616;0)>@A{TK|55PV@70 z$OfzS*(VJxQev3J?yY?O=ul(v`fp}?u9z`JK3ugibK>)DyCwImZOF4d{xK%%Ks1*} zv$oa)9anR%lXIBUqYnhLmT>VOzHfNP?ZwJNZ!5$s9M08RynIvaXw>@G^T9@r9^KH1 zVy??F&uuk)bH9Y4pQY!hP58i_H6 znl-NcuCpLV6ZWU;4C zu@9exF&OZi`Bovq_m%T+WhU2kvkz@^_LpycBvqm3bMpLw8X-Or5sL>0AKE1$(k_L=_Zc=CUq#=x1-QZf)G7nHu@fmsQ1eN_N3+nTEz`4HI4Z6uVlE zJH+X&det8JU?tO?upcM4Z=cV!JV;yF>FfL5Q$M|W_2Z!P`S=}Wzp|_1^#d%e?_H`> zV@%vA$+bFVqhw9`U;TfP|5|PD{||OiYdor8P*i??|NJcb%kzT_73*7WE?Ua5hAnR2 z=7WE=PhTlJ#ZeRznjTUb;`E(wkMZrj4e|Hilz-mK>9cZHQY**5TUPw~u}k;u73KI}xAx!0m-)GVia|x^d3p~s_9gh83jA&Ra<8rM%`>U3x69t&NzbwWY}7Ar?)FK#IZ0z|d0H0EkRO w3{9;}4Xg|ebq&m|3=9_N6z8I7$jwj5OsmAL;bP(Gi$Dzwp00i_>zopr02+f8CIA2c literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..e71a726136a47ed24125c7efc79d68a4a01961b4 GIT binary patch literal 14800 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/AppInfo.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000000..8b42559e87 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Debug.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000000..36b0fd9464 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Release.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000000..dff4f49561 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Warnings.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000000..42bcbf4780 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000000..dddb8a30c8 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Info.plist b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Info.plist new file mode 100644 index 0000000000..4789daa6a4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/MainFlutterWindow.swift b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000000..2722837ec9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000000..852fa1a472 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock new file mode 100644 index 0000000000..cce4d72430 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock @@ -0,0 +1,175 @@ +# 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.8.2" + 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.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + 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.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + flowy_editor: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + 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.2" + 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.4.9" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.17.3 <3.0.0" + flutter: ">=1.17.0" diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml new file mode 100644 index 0000000000..2149f712a8 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml @@ -0,0 +1,91 @@ +name: example +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.17.3 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + flowy_editor: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - document.json + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart b/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart new file mode 100644 index 0000000000..092d222f7e --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/favicon.png b/frontend/app_flowy/packages/flowy_editor/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-192.png b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-512.png b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-192.png b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 zcmdT|`#%%j|KDb2V@0DPm$^(Lx5}lO%Yv(=e*7hl@QqKS50#~#^IQPxBmuh|i9sXnt4ch@VT0F7% zMtrs@KWIOo+QV@lSs66A>2pz6-`9Jk=0vv&u?)^F@HZ)-6HT=B7LF;rdj zskUyBfbojcX#CS>WrIWo9D=DIwcXM8=I5D{SGf$~=gh-$LwY?*)cD%38%sCc?5OsX z-XfkyL-1`VavZ?>(pI-xp-kYq=1hsnyP^TLb%0vKRSo^~r{x?ISLY1i7KjSp z*0h&jG(Rkkq2+G_6eS>n&6>&Xk+ngOMcYrk<8KrukQHzfx675^^s$~<@d$9X{VBbg z2Fd4Z%g`!-P}d#`?B4#S-9x*eNlOVRnDrn#jY@~$jfQ-~3Od;A;x-BI1BEDdvr`pI z#D)d)!2_`GiZOUu1crb!hqH=ezs0qk<_xDm_Kkw?r*?0C3|Io6>$!kyDl;eH=aqg$B zsH_|ZD?jP2dc=)|L>DZmGyYKa06~5?C2Lc0#D%62p(YS;%_DRCB1k(+eLGXVMe+=4 zkKiJ%!N6^mxqM=wq`0+yoE#VHF%R<{mMamR9o_1JH8jfnJ?NPLs$9U!9!dq8 z0B{dI2!M|sYGH&9TAY34OlpIsQ4i5bnbG>?cWwat1I13|r|_inLE?FS@Hxdxn_YZN z3jfUO*X9Q@?HZ>Q{W0z60!bbGh557XIKu1?)u|cf%go`pwo}CD=0tau-}t@R2OrSH zQzZr%JfYa`>2!g??76=GJ$%ECbQh7Q2wLRp9QoyiRHP7VE^>JHm>9EqR3<$Y=Z1K^SHuwxCy-5@z3 zVM{XNNm}yM*pRdLKp??+_2&!bp#`=(Lh1vR{~j%n;cJv~9lXeMv)@}Odta)RnK|6* zC+IVSWumLo%{6bLDpn)Gz>6r&;Qs0^+Sz_yx_KNz9Dlt^ax`4>;EWrIT#(lJ_40<= z750fHZ7hI{}%%5`;lwkI4<_FJw@!U^vW;igL0k+mK)-j zYuCK#mCDK3F|SC}tC2>m$ZCqNB7ac-0UFBJ|8RxmG@4a4qdjvMzzS&h9pQmu^x&*= zGvapd1#K%Da&)8f?<9WN`2H^qpd@{7In6DNM&916TRqtF4;3`R|Nhwbw=(4|^Io@T zIjoR?tB8d*sO>PX4vaIHF|W;WVl6L1JvSmStgnRQq zTX4(>1f^5QOAH{=18Q2Vc1JI{V=yOr7yZJf4Vpfo zeHXdhBe{PyY;)yF;=ycMW@Kb>t;yE>;f79~AlJ8k`xWucCxJfsXf2P72bAavWL1G#W z;o%kdH(mYCM{$~yw4({KatNGim49O2HY6O07$B`*K7}MvgI=4x=SKdKVb8C$eJseA$tmSFOztFd*3W`J`yIB_~}k%Sd_bPBK8LxH)?8#jM{^%J_0|L z!gFI|68)G}ex5`Xh{5pB%GtlJ{Z5em*e0sH+sU1UVl7<5%Bq+YrHWL7?X?3LBi1R@_)F-_OqI1Zv`L zb6^Lq#H^2@d_(Z4E6xA9Z4o3kvf78ZDz!5W1#Mp|E;rvJz&4qj2pXVxKB8Vg0}ek%4erou@QM&2t7Cn5GwYqy%{>jI z)4;3SAgqVi#b{kqX#$Mt6L8NhZYgonb7>+r#BHje)bvaZ2c0nAvrN3gez+dNXaV;A zmyR0z@9h4@6~rJik-=2M-T+d`t&@YWhsoP_XP-NsVO}wmo!nR~QVWU?nVlQjNfgcTzE-PkfIX5G z1?&MwaeuzhF=u)X%Vpg_e@>d2yZwxl6-r3OMqDn8_6m^4z3zG##cK0Fsgq8fcvmhu z{73jseR%X%$85H^jRAcrhd&k!i^xL9FrS7qw2$&gwAS8AfAk#g_E_tP;x66fS`Mn@SNVrcn_N;EQm z`Mt3Z%rw%hDqTH-s~6SrIL$hIPKL5^7ejkLTBr46;pHTQDdoErS(B>``t;+1+M zvU&Se9@T_BeK;A^p|n^krIR+6rH~BjvRIugf`&EuX9u69`9C?9ANVL8l(rY6#mu^i z=*5Q)-%o*tWl`#b8p*ZH0I}hn#gV%|jt6V_JanDGuekR*-wF`u;amTCpGG|1;4A5$ zYbHF{?G1vv5;8Ph5%kEW)t|am2_4ik!`7q{ymfHoe^Z99c|$;FAL+NbxE-_zheYbV z3hb0`uZGTsgA5TG(X|GVDSJyJxsyR7V5PS_WSnYgwc_D60m7u*x4b2D79r5UgtL18 zcCHWk+K6N1Pg2c;0#r-)XpwGX?|Iv)^CLWqwF=a}fXUSM?n6E;cCeW5ER^om#{)Jr zJR81pkK?VoFm@N-s%hd7@hBS0xuCD0-UDVLDDkl7Ck=BAj*^ps`393}AJ+Ruq@fl9 z%R(&?5Nc3lnEKGaYMLmRzKXow1+Gh|O-LG7XiNxkG^uyv zpAtLINwMK}IWK65hOw&O>~EJ}x@lDBtB`yKeV1%GtY4PzT%@~wa1VgZn7QRwc7C)_ zpEF~upeDRg_<#w=dLQ)E?AzXUQpbKXYxkp>;c@aOr6A|dHA?KaZkL0svwB^U#zmx0 zzW4^&G!w7YeRxt<9;d@8H=u(j{6+Uj5AuTluvZZD4b+#+6Rp?(yJ`BC9EW9!b&KdPvzJYe5l7 zMJ9aC@S;sA0{F0XyVY{}FzW0Vh)0mPf_BX82E+CD&)wf2!x@{RO~XBYu80TONl3e+ zA7W$ra6LcDW_j4s-`3tI^VhG*sa5lLc+V6ONf=hO@q4|p`CinYqk1Ko*MbZ6_M05k zSwSwkvu;`|I*_Vl=zPd|dVD0lh&Ha)CSJJvV{AEdF{^Kn_Yfsd!{Pc1GNgw}(^~%)jk5~0L~ms|Rez1fiK~s5t(p1ci5Gq$JC#^JrXf?8 z-Y-Zi_Hvi>oBzV8DSRG!7dm|%IlZg3^0{5~;>)8-+Nk&EhAd(}s^7%MuU}lphNW9Q zT)DPo(ob{tB7_?u;4-qGDo!sh&7gHaJfkh43QwL|bbFVi@+oy;i;M zM&CP^v~lx1U`pi9PmSr&Mc<%HAq0DGH?Ft95)WY`P?~7O z`O^Nr{Py9M#Ls4Y7OM?e%Y*Mvrme%=DwQaye^Qut_1pOMrg^!5u(f9p(D%MR%1K>% zRGw%=dYvw@)o}Fw@tOtPjz`45mfpn;OT&V(;z75J*<$52{sB65$gDjwX3Xa!x_wE- z!#RpwHM#WrO*|~f7z}(}o7US(+0FYLM}6de>gQdtPazXz?OcNv4R^oYLJ_BQOd_l172oSK$6!1r@g+B@0ofJ4*{>_AIxfe-#xp>(1 z@Y3Nfd>fmqvjL;?+DmZk*KsfXJf<%~(gcLwEez%>1c6XSboURUh&k=B)MS>6kw9bY z{7vdev7;A}5fy*ZE23DS{J?8at~xwVk`pEwP5^k?XMQ7u64;KmFJ#POzdG#np~F&H ze-BUh@g54)dsS%nkBb}+GuUEKU~pHcYIg4vSo$J(J|U36bs0Use+3A&IMcR%6@jv$ z=+QI+@wW@?iu}Hpyzlvj-EYeop{f65GX0O%>w#0t|V z1-svWk`hU~m`|O$kw5?Yn5UhI%9P-<45A(v0ld1n+%Ziq&TVpBcV9n}L9Tus-TI)f zd_(g+nYCDR@+wYNQm1GwxhUN4tGMLCzDzPqY$~`l<47{+l<{FZ$L6(>J)|}!bi<)| zE35dl{a2)&leQ@LlDxLQOfUDS`;+ZQ4ozrleQwaR-K|@9T{#hB5Z^t#8 zC-d_G;B4;F#8A2EBL58s$zF-=SCr`P#z zNCTnHF&|X@q>SkAoYu>&s9v@zCpv9lLSH-UZzfhJh`EZA{X#%nqw@@aW^vPcfQrlPs(qQxmC|4tp^&sHy!H!2FH5eC{M@g;ElWNzlb-+ zxpfc0m4<}L){4|RZ>KReag2j%Ot_UKkgpJN!7Y_y3;Ssz{9 z!K3isRtaFtQII5^6}cm9RZd5nTp9psk&u1C(BY`(_tolBwzV_@0F*m%3G%Y?2utyS zY`xM0iDRT)yTyYukFeGQ&W@ReM+ADG1xu@ruq&^GK35`+2r}b^V!m1(VgH|QhIPDE X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-512.png b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 zcmeFZ_gj-)&^4Nb2tlbLMU<{!p(#yjqEe+=0IA_oih%ScH9@5#MNp&}Y#;;(h=A0@ zh7{>lT2MkSQ344eAvrhici!td|HJuyvJm#Y_w1Q9Yu3!26dNlO-oxUDK_C#XnW^Co z5C{VN6#{~B0)K2j7}*1Xq(Nqemv23A-6&=ZpEijkVnSwVGqLv40?n0=p;k3-U5e5+ z+z3>aS`u9DS=!wg8ROu?X4TFoW6CFLL&{GzoVT)ldhLekLM|+j3tIxRd|*5=c{=s&*vfPdBr(Fyj(v@%eQj1Soy7m4^@VRl1~@-PV7y+c!xz$8436WBn$t{=}mEdK#k`aystimGgI{(IBx$!pAwFoE9Y`^t^;> zKAD)C(Dl^s%`?q5$P|fZf8Xymrtu^Pv(7D`rn>Z-w$Ahs!z9!94WNVxrJuXfHAaxg zC6s@|Z1$7R$(!#t%Jb{{s6(Y?NoQXDYq)!}X@jKPhe`{9KQ@sAU8y-5`xt?S9$jKH zoi}6m5PcG*^{kjvt+kwPpyQzVg4o)a>;LK`aaN2x4@itBD3Aq?yWTM20VRn1rrd+2 zKO=P0rMjEGq_UqpMa`~7B|p?xAN1SCoCp}QxAv8O`jLJ5CVh@umR%c%i^)6!o+~`F zaalSTQcl5iwOLC&H)efzd{8(88mo`GI(56T<(&p7>Qd^;R1hn1Y~jN~tApaL8>##U zd65bo8)79CplWxr#z4!6HvLz&N7_5AN#x;kLG?zQ(#p|lj<8VUlKY=Aw!ATqeL-VG z42gA!^cMNPj>(`ZMEbCrnkg*QTsn*u(nQPWI9pA{MQ=IsPTzd7q5E#7+z>Ch=fx$~ z;J|?(5jTo5UWGvsJa(Sx0?S#56+8SD!I^tftyeh_{5_31l6&Hywtn`bbqYDqGZXI( zCG7hBgvksX2ak8+)hB4jnxlO@A32C_RM&g&qDSb~3kM&)@A_j1*oTO@nicGUyv+%^ z=vB)4(q!ykzT==Z)3*3{atJ5}2PV*?Uw+HhN&+RvKvZL3p9E?gHjv{6zM!A|z|UHK z-r6jeLxbGn0D@q5aBzlco|nG2tr}N@m;CJX(4#Cn&p&sLKwzLFx1A5izu?X_X4x8r@K*d~7>t1~ zDW1Mv5O&WOxbzFC`DQ6yNJ(^u9vJdj$fl2dq`!Yba_0^vQHXV)vqv1gssZYzBct!j zHr9>ydtM8wIs}HI4=E}qAkv|BPWzh3^_yLH(|kdb?x56^BlDC)diWyPd*|f!`^12_U>TD^^94OCN0lVv~Sgvs94ecpE^}VY$w`qr_>Ue zTfH~;C<3H<0dS5Rkf_f@1x$Gms}gK#&k()IC0zb^QbR!YLoll)c$Agfi6MKI0dP_L z=Uou&u~~^2onea2%XZ@>`0x^L8CK6=I{ge;|HXMj)-@o~h&O{CuuwBX8pVqjJ*o}5 z#8&oF_p=uSo~8vn?R0!AMWvcbZmsrj{ZswRt(aEdbi~;HeVqIe)-6*1L%5u$Gbs}| zjFh?KL&U(rC2izSGtwP5FnsR@6$-1toz?RvLD^k~h9NfZgzHE7m!!7s6(;)RKo2z} zB$Ci@h({l?arO+vF;s35h=|WpefaOtKVx>l399}EsX@Oe3>>4MPy%h&^3N_`UTAHJ zI$u(|TYC~E4)|JwkWW3F!Tib=NzjHs5ii2uj0^m|Qlh-2VnB#+X~RZ|`SA*}}&8j9IDv?F;(Y^1=Z0?wWz;ikB zewU>MAXDi~O7a~?jx1x=&8GcR-fTp>{2Q`7#BE#N6D@FCp`?ht-<1|y(NArxE_WIu zP+GuG=Qq>SHWtS2M>34xwEw^uvo4|9)4s|Ac=ud?nHQ>ax@LvBqusFcjH0}{T3ZPQ zLO1l<@B_d-(IS682}5KA&qT1+{3jxKolW+1zL4inqBS-D>BohA!K5++41tM@ z@xe<-qz27}LnV#5lk&iC40M||JRmZ*A##K3+!j93eouU8@q-`W0r%7N`V$cR&JV;iX(@cS{#*5Q>~4BEDA)EikLSP@>Oo&Bt1Z~&0d5)COI%3$cLB_M?dK# z{yv2OqW!al-#AEs&QFd;WL5zCcp)JmCKJEdNsJlL9K@MnPegK23?G|O%v`@N{rIRa zi^7a}WBCD77@VQ-z_v{ZdRsWYrYgC$<^gRQwMCi6);%R~uIi31OMS}=gUTE(GKmCI z$zM>mytL{uNN+a&S38^ez(UT=iSw=l2f+a4)DyCA1Cs_N-r?Q@$3KTYosY!;pzQ0k zzh1G|kWCJjc(oZVBji@kN%)UBw(s{KaYGy=i{g3{)Z+&H8t2`^IuLLKWT6lL<-C(! zSF9K4xd-|VO;4}$s?Z7J_dYqD#Mt)WCDnsR{Kpjq275uUq6`v0y*!PHyS(}Zmv)_{>Vose9-$h8P0|y;YG)Bo}$(3Z%+Gs0RBmFiW!^5tBmDK-g zfe5%B*27ib+7|A*Fx5e)2%kIxh7xWoc3pZcXS2zik!63lAG1;sC1ja>BqH7D zODdi5lKW$$AFvxgC-l-)!c+9@YMC7a`w?G(P#MeEQ5xID#<}W$3bSmJ`8V*x2^3qz zVe<^^_8GHqYGF$nIQm0Xq2kAgYtm#UC1A(=&85w;rmg#v906 zT;RyMgbMpYOmS&S9c38^40oUp?!}#_84`aEVw;T;r%gTZkWeU;;FwM@0y0adt{-OK z(vGnPSlR=Nv2OUN!2=xazlnHPM9EWxXg2EKf0kI{iQb#FoP>xCB<)QY>OAM$Dcdbm zU6dU|%Mo(~avBYSjRc13@|s>axhrPl@Sr81{RSZUdz4(=|82XEbV*JAX6Lfbgqgz584lYgi0 z2-E{0XCVON$wHfvaLs;=dqhQJ&6aLn$D#0i(FkAVrXG9LGm3pSTf&f~RQb6|1_;W> z?n-;&hrq*~L=(;u#jS`*Yvh@3hU-33y_Kv1nxqrsf>pHVF&|OKkoC)4DWK%I!yq?P z=vXo8*_1iEWo8xCa{HJ4tzxOmqS0&$q+>LroMKI*V-rxhOc%3Y!)Y|N6p4PLE>Yek>Y(^KRECg8<|%g*nQib_Yc#A5q8Io z6Ig&V>k|~>B6KE%h4reAo*DfOH)_01tE0nWOxX0*YTJgyw7moaI^7gW*WBAeiLbD?FV9GSB zPv3`SX*^GRBM;zledO`!EbdBO_J@fEy)B{-XUTVQv}Qf~PSDpK9+@I`7G7|>Dgbbu z_7sX9%spVo$%qwRwgzq7!_N;#Td08m5HV#?^dF-EV1o)Q=Oa+rs2xH#g;ykLbwtCh znUnA^dW!XjspJ;otq$yV@I^s9Up(5k7rqhQd@OLMyyxVLj_+$#Vc*}Usevp^I(^vH zmDgHc0VMme|K&X?9&lkN{yq_(If)O`oUPW8X}1R5pSVBpfJe0t{sPA(F#`eONTh_) zxeLqHMfJX#?P(@6w4CqRE@Eiza; z;^5)Kk=^5)KDvd9Q<`=sJU8rjjxPmtWMTmzcH={o$U)j=QBuHarp?=}c??!`3d=H$nrJMyr3L-& zA#m?t(NqLM?I3mGgWA_C+0}BWy3-Gj7bR+d+U?n*mN$%5P`ugrB{PeV>jDUn;eVc- zzeMB1mI4?fVJatrNyq|+zn=!AiN~<}eoM#4uSx^K?Iw>P2*r=k`$<3kT00BE_1c(02MRz4(Hq`L^M&xt!pV2 zn+#U3@j~PUR>xIy+P>51iPayk-mqIK_5rlQMSe5&tDkKJk_$i(X&;K(11YGpEc-K= zq4Ln%^j>Zi_+Ae9eYEq_<`D+ddb8_aY!N;)(&EHFAk@Ekg&41ABmOXfWTo)Z&KotA zh*jgDGFYQ^y=m)<_LCWB+v48DTJw*5dwMm_YP0*_{@HANValf?kV-Ic3xsC}#x2h8 z`q5}d8IRmqWk%gR)s~M}(Qas5+`np^jW^oEd-pzERRPMXj$kS17g?H#4^trtKtq;C?;c ztd|%|WP2w2Nzg@)^V}!Gv++QF2!@FP9~DFVISRW6S?eP{H;;8EH;{>X_}NGj^0cg@ z!2@A>-CTcoN02^r6@c~^QUa={0xwK0v4i-tQ9wQq^=q*-{;zJ{Qe%7Qd!&X2>rV@4 z&wznCz*63_vw4>ZF8~%QCM?=vfzW0r_4O^>UA@otm_!N%mH)!ERy&b!n3*E*@?9d^ zu}s^By@FAhG(%?xgJMuMzuJw2&@$-oK>n z=UF}rt%vuaP9fzIFCYN-1&b#r^Cl6RDFIWsEsM|ROf`E?O(cy{BPO2Ie~kT+^kI^i zp>Kbc@C?}3vy-$ZFVX#-cx)Xj&G^ibX{pWggtr(%^?HeQL@Z( zM-430g<{>vT*)jK4aY9(a{lSy{8vxLbP~n1MXwM527ne#SHCC^F_2@o`>c>>KCq9c(4c$VSyMl*y3Nq1s+!DF| z^?d9PipQN(mw^j~{wJ^VOXDCaL$UtwwTpyv8IAwGOg<|NSghkAR1GSNLZ1JwdGJYm zP}t<=5=sNNUEjc=g(y)1n5)ynX(_$1-uGuDR*6Y^Wgg(LT)Jp><5X|}bt z_qMa&QP?l_n+iVS>v%s2Li_;AIeC=Ca^v1jX4*gvB$?H?2%ndnqOaK5-J%7a} zIF{qYa&NfVY}(fmS0OmXA70{znljBOiv5Yod!vFU{D~*3B3Ka{P8?^ zfhlF6o7aNT$qi8(w<}OPw5fqA7HUje*r*Oa(YV%*l0|9FP9KW@U&{VSW{&b0?@y)M zs%4k1Ax;TGYuZ9l;vP5@?3oQsp3)rjBeBvQQ>^B;z5pc=(yHhHtq6|0m(h4envn_j787fizY@V`o(!SSyE7vlMT zbo=Z1c=atz*G!kwzGB;*uPL$Ei|EbZLh8o+1BUMOpnU(uX&OG1MV@|!&HOOeU#t^x zr9=w2ow!SsTuJWT7%Wmt14U_M*3XiWBWHxqCVZI0_g0`}*^&yEG9RK9fHK8e+S^m? zfCNn$JTswUVbiC#>|=wS{t>-MI1aYPLtzO5y|LJ9nm>L6*wpr_m!)A2Fb1RceX&*|5|MwrvOk4+!0p99B9AgP*9D{Yt|x=X}O% zgIG$MrTB=n-!q%ROT|SzH#A$Xm;|ym)0>1KR}Yl0hr-KO&qMrV+0Ej3d@?FcgZ+B3 ztEk16g#2)@x=(ko8k7^Tq$*5pfZHC@O@}`SmzT1(V@x&NkZNM2F#Q-Go7-uf_zKC( zB(lHZ=3@dHaCOf6C!6i8rDL%~XM@rVTJbZL09?ht@r^Z_6x}}atLjvH^4Vk#Ibf(^LiBJFqorm?A=lE zzFmwvp4bT@Nv2V>YQT92X;t9<2s|Ru5#w?wCvlhcHLcsq0TaFLKy(?nzezJ>CECqj zggrI~Hd4LudM(m{L@ezfnpELsRFVFw>fx;CqZtie`$BXRn#Ns%AdoE$-Pf~{9A8rV zf7FbgpKmVzmvn-z(g+&+-ID=v`;6=)itq8oM*+Uz**SMm_{%eP_c0{<%1JGiZS19o z@Gj7$Se~0lsu}w!%;L%~mIAO;AY-2i`9A*ZfFs=X!LTd6nWOZ7BZH2M{l2*I>Xu)0 z`<=;ObglnXcVk!T>e$H?El}ra0WmPZ$YAN0#$?|1v26^(quQre8;k20*dpd4N{i=b zuN=y}_ew9SlE~R{2+Rh^7%PA1H5X(p8%0TpJ=cqa$65XL)$#ign-y!qij3;2>j}I; ziO@O|aYfn&up5F`YtjGw68rD3{OSGNYmBnl?zdwY$=RFsegTZ=kkzRQ`r7ZjQP!H( zp4>)&zf<*N!tI00xzm-ME_a{_I!TbDCr;8E;kCH4LlL-tqLxDuBn-+xgPk37S&S2^ z2QZumkIimwz!c@!r0)j3*(jPIs*V!iLTRl0Cpt_UVNUgGZzdvs0(-yUghJfKr7;=h zD~y?OJ-bWJg;VdZ^r@vlDoeGV&8^--!t1AsIMZ5S440HCVr%uk- z2wV>!W1WCvFB~p$P$$_}|H5>uBeAe>`N1FI8AxM|pq%oNs;ED8x+tb44E) zTj{^fbh@eLi%5AqT?;d>Es5D*Fi{Bpk)q$^iF!!U`r2hHAO_?#!aYmf>G+jHsES4W zgpTKY59d?hsb~F0WE&dUp6lPt;Pm zcbTUqRryw^%{ViNW%Z(o8}dd00H(H-MmQmOiTq{}_rnwOr*Ybo7*}3W-qBT!#s0Ie z-s<1rvvJx_W;ViUD`04%1pra*Yw0BcGe)fDKUK8aF#BwBwMPU;9`!6E(~!043?SZx z13K%z@$$#2%2ovVlgFIPp7Q6(vO)ud)=*%ZSucL2Dh~K4B|%q4KnSpj#n@(0B})!9 z8p*hY@5)NDn^&Pmo;|!>erSYg`LkO?0FB@PLqRvc>4IsUM5O&>rRv|IBRxi(RX(gJ ztQ2;??L~&Mv;aVr5Q@(?y^DGo%pO^~zijld41aA0KKsy_6FeHIn?fNHP-z>$OoWer zjZ5hFQTy*-f7KENRiCE$ZOp4|+Wah|2=n@|W=o}bFM}Y@0e62+_|#fND5cwa3;P{^pEzlJbF1Yq^}>=wy8^^^$I2M_MH(4Dw{F6hm+vrWV5!q;oX z;tTNhz5`-V={ew|bD$?qcF^WPR{L(E%~XG8eJx(DoGzt2G{l8r!QPJ>kpHeOvCv#w zr=SSwMDaUX^*~v%6K%O~i)<^6`{go>a3IdfZ8hFmz&;Y@P%ZygShQZ2DSHd`m5AR= zx$wWU06;GYwXOf(%MFyj{8rPFXD};JCe85Bdp4$YJ2$TzZ7Gr#+SwCvBI1o$QP0(c zy`P51FEBV2HTisM3bHqpmECT@H!Y2-bv2*SoSPoO?wLe{M#zDTy@ujAZ!Izzky~3k zRA1RQIIoC*Mej1PH!sUgtkR0VCNMX(_!b65mo66iM*KQ7xT8t2eev$v#&YdUXKwGm z7okYAqYF&bveHeu6M5p9xheRCTiU8PFeb1_Rht0VVSbm%|1cOVobc8mvqcw!RjrMRM#~=7xibH&Fa5Imc|lZ{eC|R__)OrFg4@X_ ze+kk*_sDNG5^ELmHnZ7Ue?)#6!O)#Nv*Dl2mr#2)w{#i-;}0*_h4A%HidnmclH#;Q zmQbq+P4DS%3}PpPm7K_K3d2s#k~x+PlTul7+kIKol0@`YN1NG=+&PYTS->AdzPv!> zQvzT=)9se*Jr1Yq+C{wbK82gAX`NkbXFZ)4==j4t51{|-v!!$H8@WKA={d>CWRW+g z*`L>9rRucS`vbXu0rzA1#AQ(W?6)}1+oJSF=80Kf_2r~Qm-EJ6bbB3k`80rCv(0d` zvCf3;L2ovYG_TES%6vSuoKfIHC6w;V31!oqHM8-I8AFzcd^+_86!EcCOX|Ta9k1!s z_Vh(EGIIsI3fb&dF$9V8v(sTBC%!#<&KIGF;R+;MyC0~}$gC}}= zR`DbUVc&Bx`lYykFZ4{R{xRaUQkWCGCQlEc;!mf=+nOk$RUg*7 z;kP7CVLEc$CA7@6VFpsp3_t~m)W0aPxjsA3e5U%SfY{tp5BV5jH-5n?YX7*+U+Zs%LGR>U- z!x4Y_|4{gx?ZPJobISy991O znrmrC3otC;#4^&Rg_iK}XH(XX+eUHN0@Oe06hJk}F?`$)KmH^eWz@@N%wEc)%>?Ft z#9QAroDeyfztQ5Qe{m*#R#T%-h*&XvSEn@N$hYRTCMXS|EPwzF3IIysD2waj`vQD{ zv_#^Pgr?s~I*NE=acf@dWVRNWTr(GN0wrL)Z2=`Dr>}&ZDNX|+^Anl{Di%v1Id$_p zK5_H5`RDjJx`BW7hc85|> zHMMsWJ4KTMRHGu+vy*kBEMjz*^K8VtU=bXJYdhdZ-?jTXa$&n)C?QQIZ7ln$qbGlr zS*TYE+ppOrI@AoPP=VI-OXm}FzgXRL)OPvR$a_=SsC<3Jb+>5makX|U!}3lx4tX&L z^C<{9TggZNoeX!P1jX_K5HkEVnQ#s2&c#umzV6s2U-Q;({l+j^?hi7JnQ7&&*oOy9 z(|0asVTWUCiCnjcOnB2pN0DpuTglKq;&SFOQ3pUdye*eT<2()7WKbXp1qq9=bhMWlF-7BHT|i3TEIT77AcjD(v=I207wi-=vyiw5mxgPdTVUC z&h^FEUrXwWs9en2C{ywZp;nvS(Mb$8sBEh-*_d-OEm%~p1b2EpcwUdf<~zmJmaSTO zSX&&GGCEz-M^)G$fBvLC2q@wM$;n4jp+mt0MJFLuJ%c`tSp8$xuP|G81GEd2ci$|M z4XmH{5$j?rqDWoL4vs!}W&!?!rtj=6WKJcE>)?NVske(p;|#>vL|M_$as=mi-n-()a*OU3Okmk0wC<9y7t^D(er-&jEEak2!NnDiOQ99Wx8{S8}=Ng!e0tzj*#T)+%7;aM$ z&H}|o|J1p{IK0Q7JggAwipvHvko6>Epmh4RFRUr}$*2K4dz85o7|3#Bec9SQ4Y*;> zXWjT~f+d)dp_J`sV*!w>B%)#GI_;USp7?0810&3S=WntGZ)+tzhZ+!|=XlQ&@G@~3 z-dw@I1>9n1{+!x^Hz|xC+P#Ab`E@=vY?3%Bc!Po~e&&&)Qp85!I|U<-fCXy*wMa&t zgDk!l;gk;$taOCV$&60z+}_$ykz=Ea*)wJQ3-M|p*EK(cvtIre0Pta~(95J7zoxBN zS(yE^3?>88AL0Wfuou$BM{lR1hkrRibz=+I9ccwd`ZC*{NNqL)3pCcw^ygMmrG^Yp zn5f}Xf>%gncC=Yq96;rnfp4FQL#{!Y*->e82rHgY4Zwy{`JH}b9*qr^VA{%~Z}jtp z_t$PlS6}5{NtTqXHN?uI8ut8rOaD#F1C^ls73S=b_yI#iZDOGz3#^L@YheGd>L;<( z)U=iYj;`{>VDNzIxcjbTk-X3keXR8Xbc`A$o5# zKGSk-7YcoBYuAFFSCjGi;7b<;n-*`USs)IX z=0q6WZ=L!)PkYtZE-6)azhXV|+?IVGTOmMCHjhkBjfy@k1>?yFO3u!)@cl{fFAXnRYsWk)kpT?X{_$J=|?g@Q}+kFw|%n!;Zo}|HE@j=SFMvT8v`6Y zNO;tXN^036nOB2%=KzxB?n~NQ1K8IO*UE{;Xy;N^ZNI#P+hRZOaHATz9(=)w=QwV# z`z3+P>9b?l-@$@P3<;w@O1BdKh+H;jo#_%rr!ute{|YX4g5}n?O7Mq^01S5;+lABE+7`&_?mR_z7k|Ja#8h{!~j)| zbBX;*fsbUak_!kXU%HfJ2J+G7;inu#uRjMb|8a){=^))y236LDZ$$q3LRlat1D)%7K0!q5hT5V1j3qHc7MG9 z_)Q=yQ>rs>3%l=vu$#VVd$&IgO}Za#?aN!xY>-<3PhzS&q!N<=1Q7VJBfHjug^4|) z*fW^;%3}P7X#W3d;tUs3;`O&>;NKZBMR8au6>7?QriJ@gBaorz-+`pUWOP73DJL=M z(33uT6Gz@Sv40F6bN|H=lpcO z^AJl}&=TIjdevuDQ!w0K*6oZ2JBOhb31q!XDArFyKpz!I$p4|;c}@^bX{>AXdt7Bm zaLTk?c%h@%xq02reu~;t@$bv`b3i(P=g}~ywgSFpM;}b$zAD+=I!7`V~}ARB(Wx0C(EAq@?GuxOL9X+ffbkn3+Op0*80TqmpAq~EXmv%cq36celXmRz z%0(!oMp&2?`W)ALA&#|fu)MFp{V~~zIIixOxY^YtO5^FSox8v$#d0*{qk0Z)pNTt0QVZ^$`4vImEB>;Lo2!7K05TpY-sl#sWBz_W-aDIV`Ksabi zvpa#93Svo!70W*Ydh)Qzm{0?CU`y;T^ITg-J9nfWeZ-sbw)G@W?$Eomf%Bg2frfh5 zRm1{|E0+(4zXy){$}uC3%Y-mSA2-^I>Tw|gQx|7TDli_hB>``)Q^aZ`LJC2V3U$SABP}T)%}9g2pF9dT}aC~!rFFgkl1J$ z`^z{Arn3On-m%}r}TGF8KQe*OjSJ=T|caa_E;v89A{t@$yT^(G9=N9F?^kT*#s3qhJq!IH5|AhnqFd z0B&^gm3w;YbMNUKU>naBAO@fbz zqw=n!@--}o5;k6DvTW9pw)IJVz;X}ncbPVrmH>4x);8cx;q3UyiML1PWp%bxSiS|^ zC5!kc4qw%NSOGQ*Kcd#&$30=lDvs#*4W4q0u8E02U)7d=!W7+NouEyuF1dyH$D@G& zaFaxo9Ex|ZXA5y{eZT*i*dP~INSMAi@mvEX@q5i<&o&#sM}Df?Og8n8Ku4vOux=T% zeuw~z1hR}ZNwTn8KsQHKLwe2>p^K`YWUJEdVEl|mO21Bov!D0D$qPoOv=vJJ`)|%_ z>l%`eexY7t{BlVKP!`a^U@nM?#9OC*t76My_E_<16vCz1x_#82qj2PkWiMWgF8bM9 z(1t4VdHcJ;B~;Q%x01k_gQ0>u2*OjuEWNOGX#4}+N?Gb5;+NQMqp}Puqw2HnkYuKA zzKFWGHc&K>gwVgI1Sc9OT1s6fq=>$gZU!!xsilA$fF`kLdGoX*^t}ao@+^WBpk>`8 z4v_~gK|c2rCq#DZ+H)$3v~Hoi=)=1D==e3P zpKrRQ+>O^cyTuWJ%2}__0Z9SM_z9rptd*;-9uC1tDw4+A!=+K%8~M&+Zk#13hY$Y$ zo-8$*8dD5@}XDi19RjK6T^J~DIXbF5w&l?JLHMrf0 zLv0{7*G!==o|B%$V!a=EtVHdMwXLtmO~vl}P6;S(R2Q>*kTJK~!}gloxj)m|_LYK{ zl(f1cB=EON&wVFwK?MGn^nWuh@f95SHatPs(jcwSY#Dnl1@_gkOJ5=f`%s$ZHljRH0 z+c%lrb=Gi&N&1>^L_}#m>=U=(oT^vTA&3!xXNyqi$pdW1BDJ#^{h|2tZc{t^vag3& zAD7*8C`chNF|27itjBUo^CCDyEpJLX3&u+(L;YeeMwnXEoyN(ytoEabcl$lSgx~Ltatn}b$@j_yyMrBb03)shJE*$;Mw=;mZd&8e>IzE+4WIoH zCSZE7WthNUL$|Y#m!Hn?x7V1CK}V`KwW2D$-7&ODy5Cj;!_tTOOo1Mm%(RUt)#$@3 zhurA)t<7qik%%1Et+N1?R#hdBB#LdQ7{%-C zn$(`5e0eFh(#c*hvF>WT*07fk$N_631?W>kfjySN8^XC9diiOd#s?4tybICF;wBjp zIPzilX3{j%4u7blhq)tnaOBZ_`h_JqHXuI7SuIlNTgBk9{HIS&3|SEPfrvcE<@}E` zKk$y*nzsqZ{J{uWW9;#n=de&&h>m#A#q)#zRonr(?mDOYU&h&aQWD;?Z(22wY?t$U3qo`?{+amA$^TkxL+Ex2dh`q7iR&TPd0Ymwzo#b? zP$#t=elB5?k$#uE$K>C$YZbYUX_JgnXA`oF_Ifz4H7LEOW~{Gww&3s=wH4+j8*TU| zSX%LtJWqhr-xGNSe{;(16kxnak6RnZ{0qZ^kJI5X*It_YuynSpi(^-}Lolr{)#z_~ zw!(J-8%7Ybo^c3(mED`Xz8xecP35a6M8HarxRn%+NJBE;dw>>Y2T&;jzRd4FSDO3T zt*y+zXCtZQ0bP0yf6HRpD|WmzP;DR^-g^}{z~0x~z4j8m zucTe%k&S9Nt-?Jb^gYW1w6!Y3AUZ0Jcq;pJ)Exz%7k+mUOm6%ApjjSmflfKwBo6`B zhNb@$NHTJ>guaj9S{@DX)!6)b-Shav=DNKWy(V00k(D!v?PAR0f0vDNq*#mYmUp6> z76KxbFDw5U{{qx{BRj(>?|C`82ICKbfLxoldov-M?4Xl+3;I4GzLHyPOzYw7{WQST zPNYcx5onA%MAO9??41Po*1zW(Y%Zzn06-lUp{s<3!_9vv9HBjT02On0Hf$}NP;wF) zP<`2p3}A^~1YbvOh{ePMx$!JGUPX-tbBzp3mDZMY;}h;sQ->!p97GA)9a|tF(Gh{1$xk7 zUw?ELkT({Xw!KIr);kTRb1b|UL`r2_`a+&UFVCdJ)1T#fdh;71EQl9790Br0m_`$x z9|ZANuchFci8GNZ{XbP=+uXSJRe(;V5laQz$u18#?X*9}x7cIEbnr%<=1cX3EIu7$ zhHW6pe5M(&qEtsqRa>?)*{O;OJT+YUhG5{km|YI7I@JL_3Hwao9aXneiSA~a* z|Lp@c-oMNyeAEuUz{F?kuou3x#C*gU?lon!RC1s37gW^0Frc`lqQWH&(J4NoZg3m8 z;Lin#8Q+cFPD7MCzj}#|ws7b@?D9Q4dVjS4dpco=4yX5SSH=A@U@yqPdp@?g?qeia zH=Tt_9)G=6C2QIPsi-QipnK(mc0xXIN;j$WLf@n8eYvMk;*H-Q4tK%(3$CN}NGgO8n}fD~+>?<3UzvsrMf*J~%i;VKQHbF%TPalFi=#sgj)(P#SM^0Q=Tr>4kJVw8X3iWsP|e8tj}NjlMdWp z@2+M4HQu~3!=bZpjh;;DIDk&X}=c8~kn)FWWH z2KL1w^rA5&1@@^X%MjZ7;u(kH=YhH2pJPFQe=hn>tZd5RC5cfGYis8s9PKaxi*}-s6*W zRA^PwR=y^5Z){!(4D9-KC;0~;b*ploznFOaU`bJ_7U?qAi#mTo!&rIECRL$_y@yI27x2?W+zqDBD5~KCVYKFZLK+>ABC(Kj zeAll)KMgIlAG`r^rS{loBrGLtzhHY8$)<_S<(Dpkr(Ym@@vnQ&rS@FC*>2@XCH}M+an74WcRDcoQ+a3@A z9tYhl5$z7bMdTvD2r&jztBuo37?*k~wcU9GK2-)MTFS-lux-mIRYUuGUCI~V$?s#< z?1qAWb(?ZLm(N>%S%y10COdaq_Tm5c^%ooIxpR=`3e4C|@O5wY+eLik&XVi5oT7oe zmxH)Jd*5eo@!7t`x8!K=-+zJ-Sz)B_V$)s1pW~CDU$=q^&ABvf6S|?TOMB-RIm@CoFg>mjIQE)?+A1_3s6zmFU_oW&BqyMz1mY*IcP_2knjq5 zqw~JK(cVsmzc7*EvTT2rvpeqhg)W=%TOZ^>f`rD4|7Z5fq*2D^lpCttIg#ictgqZ$P@ru6P#f$x#KfnfTZj~LG6U_d-kE~`;kU_X)`H5so@?C zWmb!7x|xk@0L~0JFall*@ltyiL^)@3m4MqC7(7H0sH!WidId1#f#6R{Q&A!XzO1IAcIx;$k66dumt6lpUw@nL2MvqJ5^kbOVZ<^2jt5-njy|2@`07}0w z;M%I1$FCoLy`8xp8Tk)bFr;7aJeQ9KK6p=O$U0-&JYYy8woV*>b+FB?xLX`=pirYM z5K$BA(u)+jR{?O2r$c_Qvl?M{=Ar{yQ!UVsVn4k@0!b?_lA;dVz9uaQUgBH8Oz(Sb zrEs;&Ey>_ex8&!N{PmQjp+-Hlh|OA&wvDai#GpU=^-B70V0*LF=^bi+Nhe_o|azZ%~ZZ1$}LTmWt4aoB1 zPgccm$EwYU+jrdBaQFxQfn5gd(gM`Y*Ro1n&Zi?j=(>T3kmf94vdhf?AuS8>$Va#P zGL5F+VHpxdsCUa}+RqavXCobI-@B;WJbMphpK2%6t=XvKWWE|ruvREgM+|V=i6;;O zx$g=7^`$XWn0fu!gF=Xe9cMB8Z_SelD>&o&{1XFS`|nInK3BXlaeD*rc;R-#osyIS zWv&>~^TLIyBB6oDX+#>3<_0+2C4u2zK^wmHXXDD9_)kmLYJ!0SzM|%G9{pi)`X$uf zW}|%%#LgyK7m(4{V&?x_0KEDq56tk|0YNY~B(Sr|>WVz-pO3A##}$JCT}5P7DY+@W z#gJv>pA5>$|E3WO2tV7G^SuymB?tY`ooKcN3!vaQMnBNk-WATF{-$#}FyzgtJ8M^; zUK6KWSG)}6**+rZ&?o@PK3??uN{Q)#+bDP9i1W&j)oaU5d0bIWJ_9T5ac!qc?x66Q z$KUSZ`nYY94qfN_dpTFr8OW~A?}LD;Yty-BA)-be5Z3S#t2Io%q+cAbnGj1t$|qFR z9o?8B7OA^KjCYL=-!p}w(dkC^G6Nd%_I=1))PC0w5}ZZGJxfK)jP4Fwa@b-SYBw?% zdz9B-<`*B2dOn(N;mcTm%Do)rIvfXRNFX&1h`?>Rzuj~Wx)$p13nrDlS8-jwq@e@n zNIj_|8or==8~1h*Ih?w*8K7rYkGlwlTWAwLKc5}~dfz3y`kM&^Q|@C%1VAp_$wnw6zG~W4O+^ z>i?NY?oXf^Puc~+fDM$VgRNBpOZj{2cMP~gCqWAX4 z7>%$ux8@a&_B(pt``KSt;r+sR-$N;jdpY>|pyvPiN)9ohd*>mVST3wMo)){`B(&eX z1?zZJ-4u9NZ|~j1rdZYq4R$?swf}<6(#ex%7r{kh%U@kT)&kWuAszS%oJts=*OcL9 zaZwK<5DZw%1IFHXgFplP6JiL^dk8+SgM$D?8X+gE4172hXh!WeqIO>}$I9?Nry$*S zQ#f)RuH{P7RwA3v9f<-w>{PSzom;>(i&^l{E0(&Xp4A-*q-@{W1oE3K;1zb{&n28dSC2$N+6auXe0}e4b z)KLJ?5c*>@9K#I^)W;uU_Z`enquTUxr>mNq z1{0_puF-M7j${rs!dxxo3EelGodF1TvjV;Zpo;s{5f1pyCuRp=HDZ?s#IA4f?h|-p zGd|Mq^4hDa@Bh!c4ZE?O&x&XZ_ptZGYK4$9F4~{%R!}G1leCBx`dtNUS|K zL-7J5s4W@%mhXg1!}a4PD%!t&Qn%f_oquRajn3@C*)`o&K9o7V6DwzVMEhjVdDJ1fjhr#@=lp#@4EBqi=CCQ>73>R(>QKPNM&_Jpe5G`n4wegeC`FYEPJ{|vwS>$-`fuRSp3927qOv|NC3T3G-0 zA{K`|+tQy1yqE$ShWt8ny&5~)%ITb@^+x$w0)f&om;P8B)@}=Wzy59BwUfZ1vqw87 za2lB8J(&*l#(V}Id8SyQ0C(2amzkz3EqG&Ed0Jq1)$|&>4_|NIe=5|n=3?siFV0fI z{As5DLW^gs|B-b4C;Hd(SM-S~GQhzb>HgF2|2Usww0nL^;x@1eaB)=+Clj+$fF@H( z-fqP??~QMT$KI-#m;QC*&6vkp&8699G3)Bq0*kFZXINw=b9OVaed(3(3kS|IZ)CM? zJdnW&%t8MveBuK21uiYj)_a{Fnw0OErMzMN?d$QoPwkhOwcP&p+t>P)4tHlYw-pPN z^oJ=uc$Sl>pv@fZH~ZqxSvdhF@F1s=oZawpr^-#l{IIOGG=T%QXjtwPhIg-F@k@uIlr?J->Ia zpEUQ*=4g|XYn4Gez&aHr*;t$u3oODPmc2Ku)2Og|xjc%w;q!Zz+zY)*3{7V8bK4;& zYV82FZ+8?v)`J|G1w4I0fWdKg|2b#iaazCv;|?(W-q}$o&Y}Q5d@BRk^jL7#{kbCK zSgkyu;=DV+or2)AxCBgq-nj5=@n^`%T#V+xBGEkW4lCqrE)LMv#f;AvD__cQ@Eg3`~x| zW+h9mofSXCq5|M)9|ez(#X?-sxB%Go8};sJ?2abp(Y!lyi>k)|{M*Z$c{e1-K4ky` MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/index.html b/frontend/app_flowy/packages/flowy_editor/example/web/index.html new file mode 100644 index 0000000000..41b3bc336f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/web/index.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/manifest.json b/frontend/app_flowy/packages/flowy_editor/example/web/manifest.json new file mode 100644 index 0000000000..096edf8fe4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/windows/.gitignore new file mode 100644 index 0000000000..d492d0d98c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt new file mode 100644 index 0000000000..c0270746b1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000000..930d2071a3 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000000..8b6d4680af --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000000..dc139d85a9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000000..b93c4c30c1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + 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/frontend/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000000..b9e550fba8 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/Runner.rc b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/Runner.rc new file mode 100644 index 0000000000..5fdea291cf --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000000..b43b9095ea --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h new file mode 100644 index 0000000000..6da0652f05 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp new file mode 100644 index 0000000000..bcb57b0e2a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/resource.h b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/resource.h new file mode 100644 index 0000000000..66a65d1e4a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/resources/app_icon.ico b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000000..c977c4a425 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.cpp b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.cpp new file mode 100644 index 0000000000..f5bf9fa0f5 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.h b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.h new file mode 100644 index 0000000000..3879d54755 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.cpp b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000000..c10f08dc7d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.h b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.h new file mode 100644 index 0000000000..17ba431125 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 4a71c0211d..c56cdc1b30 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -1,9 +1,7 @@ library flowy_editor; -import 'package:flowy_editor/document/state_tree.dart'; - -class Example { - StateTree createStateTree(Map json) { - return StateTree.fromJson(json); - } -} +export 'package:flowy_editor/document/state_tree.dart'; +export 'package:flowy_editor/document/node.dart'; +export 'package:flowy_editor/document/path.dart'; +export 'package:flowy_editor/render/render_plugins.dart'; +export 'package:flowy_editor/render/base_node_widget.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/base_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/base_node_widget.dart new file mode 100644 index 0000000000..0838846a6a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/base_node_widget.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import '../document/node.dart'; +import '../render/render_plugins.dart'; + +class BaseNodeWidget extends StatefulWidget { + final T node; + final RenderPlugins renderPlugins; + + const BaseNodeWidget({ + Key? key, + required this.node, + required this.renderPlugins, + }) : super(key: key); + + @override + State createState() => _BaseNodeWidgetState(); +} + +class _BaseNodeWidgetState extends State { + @override + Widget build(BuildContext context) { + throw UnimplementedError(); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart new file mode 100644 index 0000000000..f192281cd2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; +import '../document/node.dart'; +import '../render/base_node_widget.dart'; + +typedef NodeWidgetBuilder = BaseNodeWidget Function( + T node, + RenderPlugins plugins, +); + +// unused +typedef NodeBuilder = T Function(Node node); + +class RenderPlugins { + Map nodeWidgetBuilders = {}; + // unused + // Map nodeBuilders = {}; + + void register(String name, NodeWidgetBuilder builder) { + nodeWidgetBuilders[name] = builder; + } + + void unRegister(String name) { + nodeWidgetBuilders.removeWhere((key, _) => key == name); + } + + BaseNodeWidget buildWidgetWithNode(Node node) { + final nodeWidgetBuilder = _nodeWidgetBuilder(node.type); + return nodeWidgetBuilder(node, this); + } + + NodeWidgetBuilder _nodeWidgetBuilder(String name) { + assert(nodeWidgetBuilders.containsKey(name)); + return nodeWidgetBuilders[name]!; + } +} From e3c489612eefb91d99e1bc9861a8f1161a1bac54 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 12 Jul 2022 18:39:21 +0800 Subject: [PATCH 009/121] feat: add image node widget in example --- .../flowy_editor/example/assets/document.json | 14 +++++ .../flowy_editor/example/lib/main.dart | 14 +++-- .../example/lib/plugin/image_node_widget.dart | 55 +++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 4613a523aa..49c082f461 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -28,6 +28,20 @@ "font-size": 30, "content": "cccccccccccccccccccccc" } + }, + { + "type": "image", + "attributes": { + "image_src": "https://images.pexels.com/photos/12499889/pexels-photo-12499889.jpeg?fm=jpg&w=640&h=427" + } + }, + { + "type": "text", + "attributes": { + "text-type": "heading1", + "font-size": 30, + "content": "dddddddddddddddddddd" + } } ] } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index d8ad964205..ae1f1dbcb9 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:example/plugin/image_node_widget.dart'; import 'package:example/plugin/text_node_widget.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; @@ -58,10 +59,15 @@ class _MyHomePageState extends State { void initState() { super.initState(); - renderPlugins.register( - 'text', - textNodeWidgetBuilder, - ); + renderPlugins + ..register( + 'text', + textNodeWidgetBuilder, + ) + ..register( + 'image', + imageNodeWidgetBuilder, + ); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart new file mode 100644 index 0000000000..5d4ac64dec --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -0,0 +1,55 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; + +NodeWidgetBuilder imageNodeWidgetBuilder = + (node, renderPlugins) => ImageNodeWidget( + node: node, + renderPlugins: renderPlugins, + ); + +class ImageNodeWidget extends BaseNodeWidget { + const ImageNodeWidget({ + super.key, + required super.node, + required super.renderPlugins, + }); + + @override + State createState() => _ImageNodeWidgetState(); +} + +class _ImageNodeWidgetState extends State { + Node get node => widget.node; + String get src => node.attributes['image_src'] as String; + + @override + Widget build(BuildContext context) { + final childWidget = renderChildren(); + final image = Image.network(src); + if (childWidget != null) { + return Column( + children: [image, childWidget], + ); + } else { + return image; + } + } + + // manage children's render + Widget? renderChildren() { + if (node.children.isEmpty) { + return null; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => widget.renderPlugins.buildWidgetWithNode( + e, + ), + ) + .toList(), + ); + } +} From cf0da22d6348b73c4598f4529cf42e03fb34cb78 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 12 Jul 2022 21:27:13 +0800 Subject: [PATCH 010/121] refactor: abstract node widget builder --- .../flowy_editor/example/lib/main.dart | 4 +- .../example/lib/plugin/image_node_widget.dart | 50 ++++------------- .../example/lib/plugin/text_node_widget.dart | 55 +++++-------------- .../flowy_editor/lib/flowy_editor.dart | 2 +- .../lib/render/base_node_widget.dart | 24 -------- .../lib/render/node_widget_builder.dart | 29 ++++++++++ .../lib/render/render_plugins.dart | 25 +++++---- 7 files changed, 68 insertions(+), 121 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/base_node_widget.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index ae1f1dbcb9..68a10783a6 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -62,11 +62,11 @@ class _MyHomePageState extends State { renderPlugins ..register( 'text', - textNodeWidgetBuilder, + TextNodeBuilder.create, ) ..register( 'image', - imageNodeWidgetBuilder, + ImageNodeBuilder.create, ); } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 5d4ac64dec..02d6d8fc85 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,55 +1,25 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; -NodeWidgetBuilder imageNodeWidgetBuilder = - (node, renderPlugins) => ImageNodeWidget( - node: node, - renderPlugins: renderPlugins, - ); +class ImageNodeBuilder extends NodeWidgetBuilder { + ImageNodeBuilder.create({required super.node, required super.renderPlugins}) + : super.create(); -class ImageNodeWidget extends BaseNodeWidget { - const ImageNodeWidget({ - super.key, - required super.node, - required super.renderPlugins, - }); - - @override - State createState() => _ImageNodeWidgetState(); -} - -class _ImageNodeWidgetState extends State { - Node get node => widget.node; String get src => node.attributes['image_src'] as String; @override - Widget build(BuildContext context) { - final childWidget = renderChildren(); + Widget build() { + final childrenWidget = buildChildren(); final image = Image.network(src); - if (childWidget != null) { + if (childrenWidget != null) { return Column( - children: [image, childWidget], + children: [ + image, + childrenWidget, + ], ); } else { return image; } } - - // manage children's render - Widget? renderChildren() { - if (node.children.isEmpty) { - return null; - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => widget.renderPlugins.buildWidgetWithNode( - e, - ), - ) - .toList(), - ); - } } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 6b1617d7d8..c73878c269 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -1,61 +1,32 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; -NodeWidgetBuilder textNodeWidgetBuilder = - (node, renderPlugins) => TextNodeWidget( - node: node, - renderPlugins: renderPlugins, - ); +class TextNodeBuilder extends NodeWidgetBuilder { + TextNodeBuilder.create({required super.node, required super.renderPlugins}) + : super.create(); -class TextNodeWidget extends BaseNodeWidget { - const TextNodeWidget({ - super.key, - required super.node, - required super.renderPlugins, - }); + String get content => node.attributes['content'] as String; @override - State createState() => _TextNodeWidgetState(); -} - -class _TextNodeWidgetState extends State { - Node get node => widget.node; - - @override - Widget build(BuildContext context) { - final childWidget = renderChildren(); - final richText = RichText( - text: TextSpan( + Widget build() { + final childrenWidget = buildChildren(); + final richText = SelectableText.rich( + TextSpan( text: node.attributes['content'] as String, style: node.attributes.toTextStyle(), ), ); - if (childWidget != null) { + if (childrenWidget != null) { return Column( - children: [richText, childWidget], + children: [ + richText, + childrenWidget, + ], ); } else { return richText; } } - - // manage children's render - Widget? renderChildren() { - if (node.children.isEmpty) { - return null; - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => widget.renderPlugins.buildWidgetWithNode( - e, - ), - ) - .toList(), - ); - } } extension on Attributes { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index c56cdc1b30..b16fe82273 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -4,4 +4,4 @@ export 'package:flowy_editor/document/state_tree.dart'; export 'package:flowy_editor/document/node.dart'; export 'package:flowy_editor/document/path.dart'; export 'package:flowy_editor/render/render_plugins.dart'; -export 'package:flowy_editor/render/base_node_widget.dart'; +export 'package:flowy_editor/render/node_widget_builder.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/base_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/base_node_widget.dart deleted file mode 100644 index 0838846a6a..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/base_node_widget.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import '../document/node.dart'; -import '../render/render_plugins.dart'; - -class BaseNodeWidget extends StatefulWidget { - final T node; - final RenderPlugins renderPlugins; - - const BaseNodeWidget({ - Key? key, - required this.node, - required this.renderPlugins, - }) : super(key: key); - - @override - State createState() => _BaseNodeWidgetState(); -} - -class _BaseNodeWidgetState extends State { - @override - Widget build(BuildContext context) { - throw UnimplementedError(); - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart new file mode 100644 index 0000000000..3d32c52bab --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import '../document/node.dart'; +import '../render/render_plugins.dart'; + +class NodeWidgetBuilder { + final T node; + final RenderPlugins renderPlugins; + + NodeWidgetBuilder.create({required this.node, required this.renderPlugins}); + + Widget call() => build(); + Widget build() => throw UnimplementedError(); + Widget? buildChildren() { + if (node.children.isEmpty) { + return null; + } + + // default layout + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => renderPlugins.buildWidgetWithNode(e), + ) + .toList(), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart index f192281cd2..84e3d894b7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -1,21 +1,22 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import '../document/node.dart'; -import '../render/base_node_widget.dart'; +import 'node_widget_builder.dart'; -typedef NodeWidgetBuilder = BaseNodeWidget Function( - T node, - RenderPlugins plugins, -); +typedef NodeWidgetBuilderF = A + Function({ + required T node, + required RenderPlugins renderPlugins, +}); // unused -typedef NodeBuilder = T Function(Node node); +// typedef NodeBuilder = T Function(Node node); class RenderPlugins { - Map nodeWidgetBuilders = {}; + Map nodeWidgetBuilders = {}; // unused // Map nodeBuilders = {}; - void register(String name, NodeWidgetBuilder builder) { + void register(String name, NodeWidgetBuilderF builder) { nodeWidgetBuilders[name] = builder; } @@ -23,12 +24,12 @@ class RenderPlugins { nodeWidgetBuilders.removeWhere((key, _) => key == name); } - BaseNodeWidget buildWidgetWithNode(Node node) { + Widget buildWidgetWithNode(Node node) { final nodeWidgetBuilder = _nodeWidgetBuilder(node.type); - return nodeWidgetBuilder(node, this); + return nodeWidgetBuilder(node: node, renderPlugins: this)(); } - NodeWidgetBuilder _nodeWidgetBuilder(String name) { + NodeWidgetBuilderF _nodeWidgetBuilder(String name) { assert(nodeWidgetBuilders.containsKey(name)); return nodeWidgetBuilders[name]!; } From 88f73bfbd4b6173b21bd88be606371bee7045af3 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 13 Jul 2022 10:28:52 +0800 Subject: [PATCH 011/121] chore: add comment to RenderPlugins --- .../packages/flowy_editor/lib/render/render_plugins.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart index 84e3d894b7..49929f17e1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -16,10 +16,14 @@ class RenderPlugins { // unused // Map nodeBuilders = {}; + /// register plugin to render specified [name]. + /// [name] should be correspond to the [type] in [Node]. + /// [name] could be empty. void register(String name, NodeWidgetBuilderF builder) { nodeWidgetBuilders[name] = builder; } + /// unRegister plugin with specified [name]. void unRegister(String name) { nodeWidgetBuilders.removeWhere((key, _) => key == name); } From 05786d12556145cdc211ba9e8bd597560c869b0d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 13 Jul 2022 10:51:05 +0800 Subject: [PATCH 012/121] feat: pass BuildContext to NodeWidgetBuilder --- .../flowy_editor/example/lib/main.dart | 7 ++++-- .../example/lib/plugin/image_node_widget.dart | 20 +++++++++++++---- .../example/lib/plugin/text_node_widget.dart | 22 +++++++++++++++---- .../lib/render/node_widget_builder.dart | 20 ++++------------- .../lib/render/render_plugins.dart | 16 ++++++++++---- 5 files changed, 55 insertions(+), 30 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 68a10783a6..d697fe1a9c 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -88,8 +88,11 @@ class _MyHomePageState extends State { } else { final data = Map.from(json.decode(snapshot.data!)); final stateTree = StateTree.fromJson(data); - return renderPlugins.buildWidgetWithNode( - stateTree.root, + return renderPlugins.buildWidget( + NodeWidgetContext( + buildContext: context, + node: stateTree.root, + ), ); } }, diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 02d6d8fc85..cb9d44da60 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -8,14 +8,26 @@ class ImageNodeBuilder extends NodeWidgetBuilder { String get src => node.attributes['image_src'] as String; @override - Widget build() { - final childrenWidget = buildChildren(); + Widget build(BuildContext buildContext) { final image = Image.network(src); - if (childrenWidget != null) { + Widget? children; + if (node.children.isNotEmpty) { + children = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => renderPlugins.buildWidget( + NodeWidgetContext(buildContext: buildContext, node: e), + ), + ) + .toList(), + ); + } + if (children != null) { return Column( children: [ image, - childrenWidget, + children, ], ); } else { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index c73878c269..7acf35cec9 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -8,19 +8,33 @@ class TextNodeBuilder extends NodeWidgetBuilder { String get content => node.attributes['content'] as String; @override - Widget build() { - final childrenWidget = buildChildren(); + Widget build(BuildContext buildContext) { final richText = SelectableText.rich( TextSpan( text: node.attributes['content'] as String, style: node.attributes.toTextStyle(), ), ); - if (childrenWidget != null) { + + Widget? children; + if (node.children.isNotEmpty) { + children = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => renderPlugins.buildWidget( + NodeWidgetContext(buildContext: buildContext, node: e), + ), + ) + .toList(), + ); + } + + if (children != null) { return Column( children: [ richText, - childrenWidget, + children, ], ); } else { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index 3d32c52bab..484b38ceb4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -9,21 +9,9 @@ class NodeWidgetBuilder { NodeWidgetBuilder.create({required this.node, required this.renderPlugins}); - Widget call() => build(); - Widget build() => throw UnimplementedError(); - Widget? buildChildren() { - if (node.children.isEmpty) { - return null; - } + Widget call(BuildContext buildContext) => build(buildContext); - // default layout - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => renderPlugins.buildWidgetWithNode(e), - ) - .toList(), - ); - } + /// Render the current [Node] + /// and the layout style of [Node.Children]. + Widget build(BuildContext buildContext) => throw UnimplementedError(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart index 49929f17e1..49d5dc9e1f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -2,6 +2,12 @@ import 'package:flutter/material.dart'; import '../document/node.dart'; import 'node_widget_builder.dart'; +class NodeWidgetContext { + BuildContext buildContext; + Node node; + NodeWidgetContext({required this.buildContext, required this.node}); +} + typedef NodeWidgetBuilderF = A Function({ required T node, @@ -28,13 +34,15 @@ class RenderPlugins { nodeWidgetBuilders.removeWhere((key, _) => key == name); } - Widget buildWidgetWithNode(Node node) { - final nodeWidgetBuilder = _nodeWidgetBuilder(node.type); - return nodeWidgetBuilder(node: node, renderPlugins: this)(); + Widget buildWidget(NodeWidgetContext context) { + final nodeWidgetBuilder = _nodeWidgetBuilder(context.node.type); + return nodeWidgetBuilder(node: context.node, renderPlugins: this)( + context.buildContext); } NodeWidgetBuilderF _nodeWidgetBuilder(String name) { - assert(nodeWidgetBuilders.containsKey(name)); + assert(nodeWidgetBuilders.containsKey(name), + 'Could not query the builder with this $name'); return nodeWidgetBuilders[name]!; } } From 2881edd50529c3e2bd8fda4b4552feeb73d18618 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 12 Jul 2022 20:36:04 +0800 Subject: [PATCH 013/121] feat: implement text delta operation --- .../flowy_editor/lib/document/position.dart | 5 +- .../flowy_editor/lib/document/selection.dart | 3 +- .../flowy_editor/lib/document/text_delta.dart | 413 ++++++++++++++++++ .../flowy_editor/lib/document/text_node.dart | 13 + .../flowy_editor/lib/editor_state.dart | 1 - .../flowy_editor/test/delta_test.dart | 175 ++++++++ 6 files changed, 604 insertions(+), 6 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/test/delta_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart index 2c7d85f908..88941cd82e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart @@ -12,7 +12,7 @@ class Position { }); @override - bool operator==(Object other) { + bool operator ==(Object other) { if (other is! Position) { return false; } @@ -22,7 +22,6 @@ class Position { @override int get hashCode { final pathHash = hashList(path); - return pathHash ^ offset; + return Object.hash(pathHash, offset); } - } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart index a03ccae37f..dea3a2b752 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -13,7 +13,7 @@ class Selection { return Selection(start: pos, end: pos); } - Selection collapse({ bool atStart = false }) { + Selection collapse({bool atStart = false}) { if (atStart) { return Selection(start: start, end: start); } else { @@ -24,5 +24,4 @@ class Selection { bool isCollapsed() { return start == end; } - } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart new file mode 100644 index 0000000000..8d05099291 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -0,0 +1,413 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import './node.dart'; + +// constant number: 2^53 - 1 +const int _maxInt = 9007199254740991; + +class TextOperation { + bool get isEmpty { + return length == 0; + } + + int get length { + return 0; + } + + Attributes? get attributes { + return null; + } +} + +int _hashAttributes(Attributes attributes) { + return Object.hashAllUnordered(attributes.entries); +} + +class TextInsert extends TextOperation { + String content; + final Attributes? _attributes; + + TextInsert(this.content, [Attributes? attrs]) : _attributes = attrs; + + @override + int get length { + return content.length; + } + + @override + Attributes? get attributes { + return _attributes; + } + + @override + bool operator ==(Object other) { + if (other is! TextInsert) { + return false; + } + return content == other.content && + mapEquals(_attributes, other._attributes); + } + + @override + int get hashCode { + final contentHash = content.hashCode; + final attrs = _attributes; + return Object.hash( + contentHash, attrs == null ? null : _hashAttributes(attrs)); + } +} + +class TextRetain extends TextOperation { + int _length; + final Attributes? _attributes; + + TextRetain({ + required length, + attributes, + }) : _length = length, + _attributes = attributes; + + @override + bool get isEmpty { + return length == 0; + } + + @override + int get length { + return _length; + } + + set length(int v) { + _length = v; + } + + @override + Attributes? get attributes { + return _attributes; + } + + @override + bool operator ==(Object other) { + if (other is! TextRetain) { + return false; + } + return _length == other.length && mapEquals(_attributes, other._attributes); + } + + @override + int get hashCode { + final attrs = _attributes; + return Object.hash(_length, attrs == null ? null : _hashAttributes(attrs)); + } +} + +class TextDelete extends TextOperation { + int _length; + + TextDelete({ + required int length, + }) : _length = length; + + @override + bool get isEmpty { + return length == 0; + } + + @override + int get length { + return _length; + } + + set length(int v) { + _length = v; + } + + @override + bool operator ==(Object other) { + if (other is! TextDelete) { + return false; + } + return _length == other.length; + } + + @override + int get hashCode { + return _length.hashCode; + } +} + +class _OpIterator { + final List _operations; + int _index = 0; + int _offset = 0; + + _OpIterator(List operations) : _operations = operations; + + bool get hasNext { + return peekLength() < _maxInt; + } + + TextOperation? peek() { + if (_index >= _operations.length) { + return null; + } + + return _operations[_index]; + } + + int peekLength() { + if (_index < _operations.length) { + final op = _operations[_index]; + return op.length - _offset; + } + return _maxInt; + } + + TextOperation next([int? length]) { + length ??= _maxInt; + + if (_index >= _operations.length) { + return TextRetain(length: _maxInt); + } + + final nextOp = _operations[_index]; + + final offset = _offset; + final opLength = nextOp.length; + if (length >= opLength - offset) { + length = opLength - offset; + _index += 1; + _offset = 0; + } else { + _offset += length; + } + if (nextOp is TextDelete) { + return TextDelete(length: length); + } + + if (nextOp is TextRetain) { + return TextRetain(length: length, attributes: nextOp.attributes); + } + + if (nextOp is TextInsert) { + return TextInsert( + nextOp.content.substring(offset, offset + length), nextOp.attributes); + } + + return TextRetain(length: _maxInt); + } + + List rest() { + if (!hasNext) { + return []; + } else if (_offset == 0) { + return _operations.sublist(_index); + } else { + final offset = _offset; + final index = _index; + final _next = next(); + final rest = _operations.sublist(_index); + _offset = offset; + _index = index; + return [_next] + rest; + } + } +} + +// basically copy from: https://github.com/quilljs/delta +class Delta { + final List operations; + + Delta([List? ops]) : operations = ops ?? []; + + Delta add(TextOperation textOp) { + if (textOp.isEmpty) { + return this; + } + + if (operations.isNotEmpty) { + final lastOp = operations.last; + if (lastOp is TextDelete && textOp is TextDelete) { + lastOp.length += textOp.length; + return this; + } + if (mapEquals(lastOp.attributes, textOp.attributes)) { + if (lastOp is TextInsert && textOp is TextInsert) { + lastOp.content += textOp.content; + return this; + } + // if there is an delete before the insert + // swap the order + if (lastOp is TextDelete && textOp is TextInsert) { + operations.removeLast(); + operations.add(textOp); + operations.add(lastOp); + return this; + } + if (lastOp is TextRetain && textOp is TextRetain) { + lastOp.length += textOp.length; + return this; + } + } + } + + operations.add(textOp); + return this; + } + + Delta slice(int start, [int? end]) { + final result = Delta(); + final iterator = _OpIterator(operations); + int index = 0; + + while ((end == null || index < end) && iterator.hasNext) { + TextOperation? nextOp; + if (index < start) { + nextOp = iterator.next(start - index); + } else { + nextOp = iterator.next(end == null ? null : end - index); + result.add(nextOp); + } + + index += nextOp.length; + } + + return result; + } + + Delta insert(String content, [Attributes? attributes]) { + final op = TextInsert(content, attributes); + return add(op); + } + + Delta retain(int length, [Attributes? attributes]) { + final op = TextRetain(length: length, attributes: attributes); + return add(op); + } + + Delta delete(int length) { + final op = TextDelete(length: length); + return add(op); + } + + int get length { + return operations.fold( + 0, (previousValue, element) => previousValue + element.length); + } + + Delta compose(Delta other) { + final thisIter = _OpIterator(operations); + final otherIter = _OpIterator(other.operations); + final ops = []; + + final firstOther = otherIter.peek(); + if (firstOther != null && + firstOther is TextRetain && + firstOther.attributes == null) { + int firstLeft = firstOther.length; + while ( + thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) { + firstLeft -= thisIter.peekLength(); + final next = thisIter.next(); + ops.add(next); + } + if (firstOther.length - firstLeft > 0) { + otherIter.next(firstOther.length - firstLeft); + } + } + + final delta = Delta(ops); + while (thisIter.hasNext || otherIter.hasNext) { + if (otherIter.peek() is TextInsert) { + final next = otherIter.next(); + delta.add(next); + } else if (thisIter.peek() is TextDelete) { + final next = thisIter.next(); + delta.add(next); + } else { + // otherIs + final length = min(thisIter.peekLength(), otherIter.peekLength()); + final thisOp = thisIter.next(length); + final otherOp = otherIter.next(length); + final attributes = _composeMap(thisOp.attributes, otherOp.attributes); + if (otherOp is TextRetain && otherOp.length > 0) { + TextOperation? newOp; + if (thisOp is TextRetain) { + newOp = TextRetain(length: otherOp.length, attributes: attributes); + } else if (thisOp is TextInsert) { + newOp = TextInsert(thisOp.content, attributes); + } + + if (newOp != null) { + delta.add(newOp); + } + + // Optimization if rest of other is just retain + if (!otherIter.hasNext && + delta.operations[delta.operations.length - 1] == newOp) { + final rest = Delta(thisIter.rest()); + return delta.concat(rest).chop(); + } + } else if (otherOp is TextDelete && (thisOp is TextRetain)) { + delta.add(otherOp); + } + } + } + + return delta.chop(); + } + + Delta concat(Delta other) { + var ops = [...operations]; + if (other.operations.isNotEmpty) { + ops.add(other.operations[0]); + ops = ops.sublist(1); + } + return Delta(ops); + } + + Delta chop() { + if (operations.isEmpty) { + return this; + } + final lastOp = operations.last; + if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { + operations.removeLast(); + } + return this; + } + + @override + bool operator ==(Object other) { + if (other is! Delta) { + return false; + } + return listEquals(operations, other.operations); + } + + @override + int get hashCode { + return hashList(operations); + } +} + +Attributes? _composeMap(Attributes? a, Attributes? b) { + a ??= {}; + b ??= {}; + final attributes = {}; + attributes.addAll(b); + + if (attributes.isEmpty) { + return null; + } + + for (final entry in a.entries) { + if (!b.containsKey(entry.key)) { + attributes[entry.key] = entry.value; + } + } + + return attributes; +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart new file mode 100644 index 0000000000..2e12deb939 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart @@ -0,0 +1,13 @@ + +import './text_delta.dart'; +import './node.dart'; + +class TextNode extends Node { + final Delta delta; + + TextNode( + {required super.type, + required super.children, + required super.attributes, + required this.delta}); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index d0f8c39847..15e8353725 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -28,5 +28,4 @@ class EditorState { document.delete(op.path); } } - } diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart new file mode 100644 index 0000000000..1123ad1bc0 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -0,0 +1,175 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flowy_editor/document/text_delta.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + test('test delta', () { + final delta = Delta([ + TextInsert('Gandalf', { + 'bold': true, + }), + TextInsert(' the '), + TextInsert('Grey', { + 'color': '#ccc', + }) + ]); + + final death = Delta().retain(12).insert("White", { + 'color': '#fff', + }).delete(4); + + final restores = delta.compose(death); + expect(restores.operations, [ + TextInsert('Gandalf', {'bold': true}), + TextInsert(' the '), + TextInsert('White', {'color': '#fff'}), + ]); + }); + test('compose()', () { + final a = Delta().insert('A'); + final b = Delta().insert('B'); + final expected = Delta().insert('B').insert('A'); + expect(a.compose(b), expected); + }); + test('insert + retain', () { + final a = Delta().insert('A'); + final b = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta().insert('A', { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('insert + delete', () { + final a = Delta().insert('A'); + final b = Delta().delete(1); + final expected = Delta(); + expect(a.compose(b), expected); + }); + test('delete + insert', () { + final a = Delta().delete(1); + final b = Delta().insert('B'); + final expected = Delta().insert('B').delete(1); + expect(a.compose(b), expected); + }); + test('delete + retain', () { + final a = Delta().delete(1); + final b = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta().delete(1).retain(1, { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('delete + delete', () { + final a = Delta().delete(1); + final b = Delta().delete(1); + final expected = Delta().delete(2); + expect(a.compose(b), expected); + }); + // test('retain + insert', () { + // final a = Delta().retain(1, { + // 'color': 'blue' + // }); + // final b = Delta().insert('B'); + // final expected = Delta().insert('B').retain(1, { + // 'color': 'blue', + // }); + // expect(a.compose(b), expected); + // }); + test('retain + retain', () { + final a = Delta().retain(1, { + 'color': 'blue', + }); + final b = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('retain + delete', () { + final a = Delta().retain(1, { + 'color': 'blue', + }); + final b = Delta().delete(1); + final expected = Delta().delete(1); + expect(a.compose(b), expected); + }); + // test('insert in middle of text', () { + // final a = Delta().insert('Hello'); + // final b = Delta().retain(3).insert('X'); + // final expected = Delta().insert('HElXlo'); + // expect(a.compose(b), expected); + // }); + test('insert and delete ordering', () { + final a = Delta().insert('Hello'); + final b = Delta().insert('Hello'); + final insertFirst = Delta().retain(3).insert('X').delete(1); + final deleteFirst = Delta().retain(3).delete(1).insert('X'); + final expected = Delta().insert('HelXo'); + expect(a.compose(insertFirst), expected); + expect(b.compose(deleteFirst), expected); + }); + test('delete entire text', () { + final a = Delta().retain(4).insert('Hello'); + final b = Delta().delete(9); + final expected = Delta().delete(4); + expect(a.compose(b), expected); + }); + test('retain more than length of text', () { + final a = Delta().insert('Hello'); + final b = Delta().retain(10); + final expected = Delta().insert('Hello'); + expect(a.compose(b), expected); + }); + test('retain start optimization', () { + final a = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}) + .delete(1); + final b = Delta().retain(3).insert('D'); + final expected = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}) + .insert('D') + .delete(1); + expect(a.compose(b), expected); + }); + // test('retain end optimization', () { + // final a = Delta() + // .insert('A', {'bold': true}) + // .insert('B') + // .insert('C', {'bold': true}); + // final b = Delta().delete(1); + // final expected = Delta().insert('B').insert('C', {'bold': true}); + // expect(a.compose(b), expected); + // }); + // test('retain end optimization join', () { + // final a = Delta() + // .insert('A', {'bold': true}) + // .insert('B') + // .insert('C', {'bold': true}) + // .insert('D') + // .insert('E', {'bold': true}) + // .insert('F'); + // final b = Delta().retain(1).delete(1); + // final expected = Delta() + // .insert('AC', {'bold': true}) + // .insert('D') + // .insert('E', {'bold': true}) + // .insert('F'); + // expect(a.compose(b), expected); + // }); +} From 8bd748d7cd614a1f88b49e4c69f6e6580774cc45 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 13 Jul 2022 15:27:05 +0800 Subject: [PATCH 014/121] fix: unit tests --- .../flowy_editor/lib/document/text_delta.dart | 15 ++-- .../flowy_editor/test/delta_test.dart | 82 +++++++++---------- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index 8d05099291..c53d243137 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -22,7 +22,8 @@ class TextOperation { } int _hashAttributes(Attributes attributes) { - return Object.hashAllUnordered(attributes.entries); + return Object.hashAllUnordered( + attributes.entries.map((e) => Object.hash(e.key, e.value))); } class TextInsert extends TextOperation { @@ -335,7 +336,7 @@ class Delta { if (otherOp is TextRetain && otherOp.length > 0) { TextOperation? newOp; if (thisOp is TextRetain) { - newOp = TextRetain(length: otherOp.length, attributes: attributes); + newOp = TextRetain(length: length, attributes: attributes); } else if (thisOp is TextInsert) { newOp = TextInsert(thisOp.content, attributes); } @@ -363,7 +364,7 @@ class Delta { var ops = [...operations]; if (other.operations.isNotEmpty) { ops.add(other.operations[0]); - ops = ops.sublist(1); + ops.addAll(other.operations.sublist(1)); } return Delta(ops); } @@ -399,15 +400,15 @@ Attributes? _composeMap(Attributes? a, Attributes? b) { final attributes = {}; attributes.addAll(b); - if (attributes.isEmpty) { - return null; - } - for (final entry in a.entries) { if (!b.containsKey(entry.key)) { attributes[entry.key] = entry.value; } } + if (attributes.isEmpty) { + return null; + } + return attributes; } diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index 1123ad1bc0..f90d487b18 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -73,16 +73,16 @@ void main() { final expected = Delta().delete(2); expect(a.compose(b), expected); }); - // test('retain + insert', () { - // final a = Delta().retain(1, { - // 'color': 'blue' - // }); - // final b = Delta().insert('B'); - // final expected = Delta().insert('B').retain(1, { - // 'color': 'blue', - // }); - // expect(a.compose(b), expected); - // }); + test('retain + insert', () { + final a = Delta().retain(1, { + 'color': 'blue' + }); + final b = Delta().insert('B'); + final expected = Delta().insert('B').retain(1, { + 'color': 'blue', + }); + expect(a.compose(b), expected); + }); test('retain + retain', () { final a = Delta().retain(1, { 'color': 'blue', @@ -105,12 +105,12 @@ void main() { final expected = Delta().delete(1); expect(a.compose(b), expected); }); - // test('insert in middle of text', () { - // final a = Delta().insert('Hello'); - // final b = Delta().retain(3).insert('X'); - // final expected = Delta().insert('HElXlo'); - // expect(a.compose(b), expected); - // }); + test('insert in middle of text', () { + final a = Delta().insert('Hello'); + final b = Delta().retain(3).insert('X'); + final expected = Delta().insert('HelXlo'); + expect(a.compose(b), expected); + }); test('insert and delete ordering', () { final a = Delta().insert('Hello'); final b = Delta().insert('Hello'); @@ -147,29 +147,29 @@ void main() { .delete(1); expect(a.compose(b), expected); }); - // test('retain end optimization', () { - // final a = Delta() - // .insert('A', {'bold': true}) - // .insert('B') - // .insert('C', {'bold': true}); - // final b = Delta().delete(1); - // final expected = Delta().insert('B').insert('C', {'bold': true}); - // expect(a.compose(b), expected); - // }); - // test('retain end optimization join', () { - // final a = Delta() - // .insert('A', {'bold': true}) - // .insert('B') - // .insert('C', {'bold': true}) - // .insert('D') - // .insert('E', {'bold': true}) - // .insert('F'); - // final b = Delta().retain(1).delete(1); - // final expected = Delta() - // .insert('AC', {'bold': true}) - // .insert('D') - // .insert('E', {'bold': true}) - // .insert('F'); - // expect(a.compose(b), expected); - // }); + test('retain end optimization', () { + final a = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}); + final b = Delta().delete(1); + final expected = Delta().insert('B').insert('C', {'bold': true}); + expect(a.compose(b), expected); + }); + test('retain end optimization join', () { + final a = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}) + .insert('D') + .insert('E', {'bold': true}) + .insert('F'); + final b = Delta().retain(1).delete(1); + final expected = Delta() + .insert('AC', {'bold': true}) + .insert('D') + .insert('E', {'bold': true}) + .insert('F'); + expect(a.compose(b), expected); + }); } From f2c477e89feb0905bb0ece8ecb5a090f8cec64a8 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 13 Jul 2022 16:01:48 +0800 Subject: [PATCH 015/121] feat: change attributes map to dynamic --- frontend/app_flowy/packages/flowy_editor/lib/document/node.dart | 2 +- .../packages/flowy_editor/lib/document/text_delta.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index e4ff84b99c..fd93da0bdd 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'package:flowy_editor/document/path.dart'; -typedef Attributes = Map; +typedef Attributes = Map; class Node extends LinkedListEntry { Node? parent; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index c53d243137..c799fa65c2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -397,7 +397,7 @@ class Delta { Attributes? _composeMap(Attributes? a, Attributes? b) { a ??= {}; b ??= {}; - final attributes = {}; + final Attributes attributes = {}; attributes.addAll(b); for (final entry in a.entries) { From de507001f4c54b0d4727094aec6415d37eb00471 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 13 Jul 2022 16:24:17 +0800 Subject: [PATCH 016/121] fix: use UnmodifiableListView for OpIterator --- .../packages/flowy_editor/lib/document/text_delta.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index c799fa65c2..30d3b81b9f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:math'; import 'package:flutter/foundation.dart'; @@ -140,11 +141,11 @@ class TextDelete extends TextOperation { } class _OpIterator { - final List _operations; + final UnmodifiableListView _operations; int _index = 0; int _offset = 0; - _OpIterator(List operations) : _operations = operations; + _OpIterator(List operations) : _operations = UnmodifiableListView(operations); bool get hasNext { return peekLength() < _maxInt; From 55ca05f30eb1b06f2fa3cfe9eade8c4648782eff Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 13 Jul 2022 16:09:41 +0800 Subject: [PATCH 017/121] feat: support subtype render plugin and add text with check-box example --- .../flowy_editor/assets/document.json | 4 +- .../flowy_editor/example/assets/document.json | 9 ++-- .../flowy_editor/example/lib/main.dart | 7 ++- .../example/lib/plugin/image_node_widget.dart | 28 +++++++++-- .../example/lib/plugin/text_node_widget.dart | 8 ++-- .../text_with_check_box_node_widget.dart | 27 +++++++++++ .../flowy_editor/example/pubspec.lock | 14 ++++++ .../flowy_editor/example/pubspec.yaml | 1 + .../flowy_editor/lib/document/node.dart | 21 ++++++++- .../lib/render/render_plugins.dart | 47 +++++++++++++++---- .../flowy_editor/test/flowy_editor_test.dart | 4 +- 11 files changed, 145 insertions(+), 25 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json index 8aa75717ac..31092286f8 100644 --- a/frontend/app_flowy/packages/flowy_editor/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json @@ -5,7 +5,7 @@ { "type": "text", "attributes": { - "text-type": "heading1" + "subtype": "with-heading" } }, { @@ -24,7 +24,7 @@ { "type": "text", "attributes": { - "text-type": "check-box", + "text-type": "checkbox", "check": true } }, diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 49c082f461..f41ec18130 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -8,17 +8,20 @@ { "type": "text", "attributes": { - "text-type": "heading1", + "subtype": "with-checkbox", "font-size": 30, - "content": "aaaaaaaaaaaaaaaaaaaaaaaa" + "content": "aaaaaaaaaaaaaaaaaaaaaaaa", + "checkbox": false } }, { "type": "text", "attributes": { + "subtype": "with-checkbox", "text-type": "heading1", "font-size": 30, - "content": "bbbbbbbbbbbbbbbbbbbbbbb" + "content": "bbbbbbbbbbbbbbbbbbbbbbb", + "checkbox": false } }, { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index d697fe1a9c..845b352a23 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:example/plugin/image_node_widget.dart'; import 'package:example/plugin/text_node_widget.dart'; +import 'package:example/plugin/text_with_check_box_node_widget.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/services.dart'; @@ -67,6 +68,10 @@ class _MyHomePageState extends State { ..register( 'image', ImageNodeBuilder.create, + ) + ..register( + 'text/with-checkbox', + TextWithCheckBoxNodeBuilder.create, ); } @@ -89,7 +94,7 @@ class _MyHomePageState extends State { final data = Map.from(json.decode(snapshot.data!)); final stateTree = StateTree.fromJson(data); return renderPlugins.buildWidget( - NodeWidgetContext( + context: NodeWidgetContext( buildContext: context, node: stateTree.root, ), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index cb9d44da60..f960f877df 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,14 +1,36 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class ImageNodeBuilder extends NodeWidgetBuilder { - ImageNodeBuilder.create({required super.node, required super.renderPlugins}) - : super.create(); + ImageNodeBuilder.create({ + required super.node, + required super.renderPlugins, + }) : super.create(); String get src => node.attributes['image_src'] as String; @override Widget build(BuildContext buildContext) { + Future.delayed(const Duration(seconds: 5), () { + node.updateAttributes({ + 'image_src': + "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400" + }); + }); + return ChangeNotifierProvider.value( + value: node, + builder: (context, child) { + return Consumer( + builder: (context, value, child) { + return _build(context); + }, + ); + }, + ); + } + + Widget _build(BuildContext buildContext) { final image = Image.network(src); Widget? children; if (node.children.isNotEmpty) { @@ -17,7 +39,7 @@ class ImageNodeBuilder extends NodeWidgetBuilder { children: node.children .map( (e) => renderPlugins.buildWidget( - NodeWidgetContext(buildContext: buildContext, node: e), + context: NodeWidgetContext(buildContext: buildContext, node: e), ), ) .toList(), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 7acf35cec9..825f55a4ab 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; class TextNodeBuilder extends NodeWidgetBuilder { - TextNodeBuilder.create({required super.node, required super.renderPlugins}) - : super.create(); + TextNodeBuilder.create({ + required super.node, + required super.renderPlugins, + }) : super.create(); String get content => node.attributes['content'] as String; @@ -23,7 +25,7 @@ class TextNodeBuilder extends NodeWidgetBuilder { children: node.children .map( (e) => renderPlugins.buildWidget( - NodeWidgetContext(buildContext: buildContext, node: e), + context: NodeWidgetContext(buildContext: buildContext, node: e), ), ) .toList(), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart new file mode 100644 index 0000000000..79dddaa665 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart @@ -0,0 +1,27 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; + +class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder { + TextWithCheckBoxNodeBuilder.create({ + required super.node, + required super.renderPlugins, + }) : super.create(); + + // TODO: check the type + bool get isCompleted => node.attributes['checkbox'] as bool; + + @override + Widget build(BuildContext buildContext) { + return Row( + children: [ + Checkbox(value: isCompleted, onChanged: (value) {}), + Expanded( + child: renderPlugins.buildWidget( + context: NodeWidgetContext(buildContext: buildContext, node: node), + withSubtype: false, + ), + ) + ], + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock index cce4d72430..63ade5e65c 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock @@ -109,6 +109,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -116,6 +123,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.3" sky_engine: dependency: transitive description: flutter diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml index 2149f712a8..1a788cfb7d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: cupertino_icons: ^1.0.2 flowy_editor: path: ../ + provider: ^6.0.3 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index e4ff84b99c..07c0c13ade 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,14 +1,24 @@ import 'dart:collection'; import 'package:flowy_editor/document/path.dart'; +import 'package:flutter/material.dart'; typedef Attributes = Map; -class Node extends LinkedListEntry { +class Node extends ChangeNotifier with LinkedListEntry { Node? parent; final String type; final LinkedList children; final Attributes attributes; + String? get subtype { + // TODO: make 'subtype' as a const value. + if (attributes.containsKey('subtype')) { + assert(attributes['subtype'] is String, 'subtype must be a [String]'); + return attributes['subtype'] as String; + } + return null; + } + Node({ required this.type, required this.children, @@ -53,6 +63,9 @@ class Node extends LinkedListEntry { for (final attribute in attributes.entries) { this.attributes[attribute.key] = attribute.value; } + + // Notify the new attributes + notifyListeners(); } Node? childAtIndex(int index) { @@ -75,12 +88,18 @@ class Node extends LinkedListEntry { void insertAfter(Node entry) { entry.parent = parent; super.insertAfter(entry); + + // Notify the new node. + parent?.notifyListeners(); } @override void insertBefore(Node entry) { entry.parent = parent; super.insertBefore(entry); + + // Notify the new node. + parent?.notifyListeners(); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart index 49d5dc9e1f..4dfe3a88ba 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -18,31 +18,58 @@ typedef NodeWidgetBuilderF = A // typedef NodeBuilder = T Function(Node node); class RenderPlugins { - Map nodeWidgetBuilders = {}; + final Map _nodeWidgetBuilders = {}; // unused // Map nodeBuilders = {}; - /// register plugin to render specified [name]. - /// [name] should be correspond to the [type] in [Node]. + /// Register plugin to render specified [name]. + /// + /// [name] should be [Node].type + /// or [Node].type + '/' + [Node].attributes['subtype']. + /// + /// e.g. 'text', 'text/with-checkbox', or 'text/with-heading' + /// /// [name] could be empty. void register(String name, NodeWidgetBuilderF builder) { - nodeWidgetBuilders[name] = builder; + _validatePluginName(name); + + _nodeWidgetBuilders[name] = builder; } - /// unRegister plugin with specified [name]. + /// UnRegister plugin with specified [name]. void unRegister(String name) { - nodeWidgetBuilders.removeWhere((key, _) => key == name); + _validatePluginName(name); + + _nodeWidgetBuilders.removeWhere((key, _) => key == name); } - Widget buildWidget(NodeWidgetContext context) { - final nodeWidgetBuilder = _nodeWidgetBuilder(context.node.type); + Widget buildWidget({ + required NodeWidgetContext context, + bool withSubtype = true, + }) { + /// Find node widget builder + /// 1. If node's attributes contains subtype, return. + /// 2. If node's attributes do no contains substype, return. + final node = context.node; + var name = node.type; + if (withSubtype && node.subtype != null) { + name += '/${node.subtype}'; + } + final nodeWidgetBuilder = _nodeWidgetBuilder(name); return nodeWidgetBuilder(node: context.node, renderPlugins: this)( context.buildContext); } NodeWidgetBuilderF _nodeWidgetBuilder(String name) { - assert(nodeWidgetBuilders.containsKey(name), + assert(_nodeWidgetBuilders.containsKey(name), 'Could not query the builder with this $name'); - return nodeWidgetBuilders[name]!; + return _nodeWidgetBuilders[name]!; + } + + void _validatePluginName(String name) { + final paths = name.split('/'); + if (paths.length > 2) { + throw Exception('[Name] must contains zero or one slash("/")'); + } } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index e8f14bc9c7..6e98376748 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -44,7 +44,7 @@ void main() { final stateTree = StateTree.fromJson(data); final deletedNode = stateTree.delete([1, 1]); expect(deletedNode != null, true); - expect(deletedNode!.attributes['text-type'], 'check-box'); + expect(deletedNode!.attributes['text-type'], 'checkbox'); final node = stateTree.nodeAtPath([1, 1]); expect(node != null, true); expect(node!.attributes['tag'], '**'); @@ -56,7 +56,7 @@ void main() { final stateTree = StateTree.fromJson(data); final attributes = stateTree.update([1, 1], {'text-type': 'heading1'}); expect(attributes != null, true); - expect(attributes!['text-type'], 'check-box'); + expect(attributes!['text-type'], 'checkbox'); final updatedNode = stateTree.nodeAtPath([1, 1]); expect(updatedNode != null, true); expect(updatedNode!.attributes['text-type'], 'heading1'); From 5ad7845189bab00426d93b2443d6f048f0503999 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 13 Jul 2022 18:31:18 +0800 Subject: [PATCH 018/121] feat: 1. move render plugins to editor state 2. support get node's path 3. format code --- .../flowy_editor/example/assets/document.json | 2 +- .../flowy_editor/example/lib/main.dart | 11 ++--- .../example/lib/plugin/image_node_widget.dart | 48 ++++++++++++------- .../example/lib/plugin/text_node_widget.dart | 8 +++- .../text_with_check_box_node_widget.dart | 9 +++- .../flowy_editor/lib/document/node.dart | 21 ++++++++ .../flowy_editor/lib/document/state_tree.dart | 10 +++- .../flowy_editor/lib/document/text_delta.dart | 21 ++++++-- .../flowy_editor/lib/document/text_node.dart | 12 ++--- .../flowy_editor/lib/editor_state.dart | 30 +++++++++++- .../flowy_editor/lib/flowy_editor.dart | 3 ++ .../flowy_editor/lib/operation/operation.dart | 21 ++++---- .../lib/operation/transaction.dart | 4 +- .../lib/render/node_widget_builder.dart | 17 ++++--- .../lib/render/render_plugins.dart | 23 ++++++--- .../flowy_editor/test/delta_test.dart | 4 +- .../flowy_editor/test/flowy_editor_test.dart | 15 +++++- 17 files changed, 192 insertions(+), 67 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index f41ec18130..2ddedf70b3 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -20,7 +20,7 @@ "subtype": "with-checkbox", "text-type": "heading1", "font-size": 30, - "content": "bbbbbbbbbbbbbbbbbbbbbbb", + "content": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "checkbox": false } }, diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 845b352a23..593cfa2871 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -92,13 +92,12 @@ class _MyHomePageState extends State { ); } else { final data = Map.from(json.decode(snapshot.data!)); - final stateTree = StateTree.fromJson(data); - return renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: stateTree.root, - ), + final document = StateTree.fromJson(data); + final editorState = EditorState( + document: document, + renderPlugins: renderPlugins, ); + return editorState.build(context); } }, ), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index f960f877df..0f2031c411 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -5,27 +5,39 @@ import 'package:provider/provider.dart'; class ImageNodeBuilder extends NodeWidgetBuilder { ImageNodeBuilder.create({ required super.node, - required super.renderPlugins, + required super.editorState, }) : super.create(); String get src => node.attributes['image_src'] as String; @override Widget build(BuildContext buildContext) { - Future.delayed(const Duration(seconds: 5), () { - node.updateAttributes({ - 'image_src': - "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400" - }); - }); - return ChangeNotifierProvider.value( - value: node, - builder: (context, child) { - return Consumer( - builder: (context, value, child) { - return _build(context); - }, - ); + // Future.delayed(const Duration(seconds: 5), () { + // node.updateAttributes({ + // 'image_src': + // "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400" + // }); + // }); + return GestureDetector( + child: ChangeNotifierProvider.value( + value: node, + builder: (context, child) { + return Consumer( + builder: (context, value, child) { + return _build(context); + }, + ); + }, + ), + onTap: () { + const newImageSrc = + "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400"; + final newAttribute = Attributes.from(node.attributes) + ..update( + 'image_src', + (value) => newImageSrc, + ); + editorState.update(node, newAttribute); }, ); } @@ -39,7 +51,11 @@ class ImageNodeBuilder extends NodeWidgetBuilder { children: node.children .map( (e) => renderPlugins.buildWidget( - context: NodeWidgetContext(buildContext: buildContext, node: e), + context: NodeWidgetContext( + buildContext: buildContext, + node: e, + editorState: editorState, + ), ), ) .toList(), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 825f55a4ab..482e6855f3 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -4,7 +4,7 @@ import 'package:flowy_editor/flowy_editor.dart'; class TextNodeBuilder extends NodeWidgetBuilder { TextNodeBuilder.create({ required super.node, - required super.renderPlugins, + required super.editorState, }) : super.create(); String get content => node.attributes['content'] as String; @@ -25,7 +25,11 @@ class TextNodeBuilder extends NodeWidgetBuilder { children: node.children .map( (e) => renderPlugins.buildWidget( - context: NodeWidgetContext(buildContext: buildContext, node: e), + context: NodeWidgetContext( + buildContext: buildContext, + node: e, + editorState: editorState, + ), ), ) .toList(), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart index 79dddaa665..37a30fb6be 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder { TextWithCheckBoxNodeBuilder.create({ required super.node, - required super.renderPlugins, + required super.editorState, }) : super.create(); // TODO: check the type @@ -13,11 +13,16 @@ class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder { @override Widget build(BuildContext buildContext) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Checkbox(value: isCompleted, onChanged: (value) {}), Expanded( child: renderPlugins.buildWidget( - context: NodeWidgetContext(buildContext: buildContext, node: node), + context: NodeWidgetContext( + buildContext: buildContext, + node: node, + editorState: editorState, + ), withSubtype: false, ), ) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 1e5200501f..1e951f4d79 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -84,6 +84,27 @@ class Node extends ChangeNotifier with LinkedListEntry { return childAtIndex(path.first)?.childAtPath(path.sublist(1)); } + Node root() { + if (parent != null) { + return parent!.root(); + } + return this; + } + + Path path([Path previous = const []]) { + if (parent == null) { + return previous; + } + var index = 0; + for (var child in parent!.children) { + if (child == this) { + break; + } + index += 1; + } + return parent!.path([index, ...previous]); + } + @override void insertAfter(Node entry) { entry.parent = parent; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 368b575c90..8f7f99aa13 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -4,7 +4,9 @@ import 'package:flowy_editor/document/path.dart'; class StateTree { final Node root; - StateTree({required this.root}); + StateTree({ + required this.root, + }); factory StateTree.fromJson(Attributes json) { assert(json['document'] is Map); @@ -14,6 +16,12 @@ class StateTree { return StateTree(root: root); } + // Path pathForNode(Node node) { + // var nodeRoot = node.root(); + // assert(nodeRoot == root, "Every node's root must be same as root"); + + // } + Node? nodeAtPath(Path path) { return root.childAtPath(path); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index 30d3b81b9f..a52a96426a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -24,7 +24,10 @@ class TextOperation { int _hashAttributes(Attributes attributes) { return Object.hashAllUnordered( - attributes.entries.map((e) => Object.hash(e.key, e.value))); + attributes.entries.map( + (e) => Object.hash(e.key, e.value), + ), + ); } class TextInsert extends TextOperation { @@ -145,7 +148,8 @@ class _OpIterator { int _index = 0; int _offset = 0; - _OpIterator(List operations) : _operations = UnmodifiableListView(operations); + _OpIterator(List operations) + : _operations = UnmodifiableListView(operations); bool get hasNext { return peekLength() < _maxInt; @@ -186,16 +190,23 @@ class _OpIterator { _offset += length; } if (nextOp is TextDelete) { - return TextDelete(length: length); + return TextDelete( + length: length, + ); } if (nextOp is TextRetain) { - return TextRetain(length: length, attributes: nextOp.attributes); + return TextRetain( + length: length, + attributes: nextOp.attributes, + ); } if (nextOp is TextInsert) { return TextInsert( - nextOp.content.substring(offset, offset + length), nextOp.attributes); + nextOp.content.substring(offset, offset + length), + nextOp.attributes, + ); } return TextRetain(length: _maxInt); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart index 2e12deb939..535431b615 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart @@ -1,13 +1,13 @@ - import './text_delta.dart'; import './node.dart'; class TextNode extends Node { final Delta delta; - TextNode( - {required super.type, - required super.children, - required super.attributes, - required this.delta}); + TextNode({ + required super.type, + required super.children, + required super.attributes, + required this.delta, + }); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 15e8353725..d60c06a497 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,24 +1,52 @@ +import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/operation/operation.dart'; +import 'package:flutter/material.dart'; import './document/state_tree.dart'; import './document/selection.dart'; import './operation/operation.dart'; import './operation/transaction.dart'; +import './render/render_plugins.dart'; class EditorState { final StateTree document; + final RenderPlugins renderPlugins; Selection? cursorSelection; EditorState({ required this.document, + required this.renderPlugins, }); - apply(Transaction transaction) { + /// TODO: move to a better place. + Widget build(BuildContext context) { + return renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: document.root, + editorState: this, + ), + ); + } + + void apply(Transaction transaction) { for (final op in transaction.operations) { _applyOperation(op); } } + // TODO: move to a better place. + void update( + Node node, + Attributes attributes, + ) { + _applyOperation(UpdateOperation( + path: node.path(), + attributes: attributes, + oldAttributes: node.attributes, + )); + } + _applyOperation(Operation op) { if (op is InsertOperation) { document.insert(op.path, op.value); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index b16fe82273..d4c0a8d70a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -5,3 +5,6 @@ export 'package:flowy_editor/document/node.dart'; export 'package:flowy_editor/document/path.dart'; export 'package:flowy_editor/render/render_plugins.dart'; export 'package:flowy_editor/render/node_widget_builder.dart'; +export 'package:flowy_editor/operation/transaction.dart'; +export 'package:flowy_editor/operation/operation.dart'; +export 'package:flowy_editor/editor_state.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index b5d71b57d4..4ca7b9ca83 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -2,9 +2,7 @@ import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/node.dart'; abstract class Operation { - Operation invert(); - } class InsertOperation extends Operation { @@ -18,9 +16,11 @@ class InsertOperation extends Operation { @override Operation invert() { - return DeleteOperation(path: path, removedValue: value); + return DeleteOperation( + path: path, + removedValue: value, + ); } - } class UpdateOperation extends Operation { @@ -36,9 +36,12 @@ class UpdateOperation extends Operation { @override Operation invert() { - return UpdateOperation(path: path, attributes: oldAttributes, oldAttributes: attributes); + return UpdateOperation( + path: path, + attributes: oldAttributes, + oldAttributes: attributes, + ); } - } class DeleteOperation extends Operation { @@ -52,7 +55,9 @@ class DeleteOperation extends Operation { @override Operation invert() { - return InsertOperation(path: path, value: removedValue); + return InsertOperation( + path: path, + value: removedValue, + ); } - } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart index c6fbed63aa..1a2c4bcdb5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart @@ -1,6 +1,6 @@ import './operation.dart'; class Transaction { - final List operations = []; - + final List operations; + Transaction([this.operations = const []]); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index 484b38ceb4..0dad14f821 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -1,17 +1,22 @@ +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flutter/material.dart'; -import '../document/node.dart'; -import '../render/render_plugins.dart'; - class NodeWidgetBuilder { + final EditorState editorState; final T node; - final RenderPlugins renderPlugins; - NodeWidgetBuilder.create({required this.node, required this.renderPlugins}); + RenderPlugins get renderPlugins => editorState.renderPlugins; - Widget call(BuildContext buildContext) => build(buildContext); + NodeWidgetBuilder.create({ + required this.editorState, + required this.node, + }); /// Render the current [Node] /// and the layout style of [Node.Children]. Widget build(BuildContext buildContext) => throw UnimplementedError(); + + Widget call(BuildContext buildContext) => build(buildContext); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart index 4dfe3a88ba..a9bbd8b070 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -1,17 +1,24 @@ import 'package:flutter/material.dart'; import '../document/node.dart'; -import 'node_widget_builder.dart'; +import './node_widget_builder.dart'; +import 'package:flowy_editor/editor_state.dart'; class NodeWidgetContext { - BuildContext buildContext; - Node node; - NodeWidgetContext({required this.buildContext, required this.node}); + final BuildContext buildContext; + final Node node; + final EditorState editorState; + + NodeWidgetContext({ + required this.buildContext, + required this.node, + required this.editorState, + }); } typedef NodeWidgetBuilderF = A Function({ required T node, - required RenderPlugins renderPlugins, + required EditorState editorState, }); // unused @@ -56,8 +63,10 @@ class RenderPlugins { name += '/${node.subtype}'; } final nodeWidgetBuilder = _nodeWidgetBuilder(name); - return nodeWidgetBuilder(node: context.node, renderPlugins: this)( - context.buildContext); + return nodeWidgetBuilder( + node: context.node, + editorState: context.editorState, + )(context.buildContext); } NodeWidgetBuilderF _nodeWidgetBuilder(String name) { diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index f90d487b18..3ddd01efb7 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -74,9 +74,7 @@ void main() { expect(a.compose(b), expected); }); test('retain + insert', () { - final a = Delta().retain(1, { - 'color': 'blue' - }); + final a = Delta().retain(1, {'color': 'blue'}); final b = Delta().insert('B'); final expected = Delta().insert('B').retain(1, { 'color': 'blue', diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index b284b8608d..6635f46827 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/state_tree.dart'; @@ -20,7 +21,7 @@ void main() { expect(stateTree.root.toJson(), data['document']); }); - test('search node in state tree', () async { + test('search node by Path in state tree', () async { final String response = await rootBundle.loadString('assets/document.json'); final data = Map.from(json.decode(response)); final stateTree = StateTree.fromJson(data); @@ -30,6 +31,18 @@ void main() { expect(textType != null, true); }); + test('search node by Self in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final checkBoxNode = stateTree.root.childAtPath([1, 0]); + expect(checkBoxNode != null, true); + final textType = checkBoxNode!.attributes['text-type']; + expect(textType != null, true); + final path = checkBoxNode.path([]); + expect(pathEquals(path, [1, 0]), true); + }); + test('insert node in state tree', () async { final String response = await rootBundle.loadString('assets/document.json'); final data = Map.from(json.decode(response)); From 7fad8ab3dc2c444a831e4cd2e5bd00bf2148e961 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 13 Jul 2022 18:46:38 +0800 Subject: [PATCH 019/121] chore: run flutter test in flowy_editor_branch --- .github/workflows/dart_test.yml | 6 ++++++ frontend/app_flowy/packages/flowy_editor/pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dart_test.yml b/.github/workflows/dart_test.yml index 74b20a2425..f0b42a506d 100644 --- a/.github/workflows/dart_test.yml +++ b/.github/workflows/dart_test.yml @@ -8,6 +8,7 @@ on: pull_request: branches: - 'main' + - 'feat/flowy_editor' env: CARGO_TERM_COLOR: always @@ -71,3 +72,8 @@ jobs: flutter pub get flutter test + - name: Run FlowyEditor tests + working-directory: frontend/app_flowy/packages/flowy_editor + run: | + flutter pub get + flutter test \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index a8c4f0c430..6a6d32d580 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.1 homepage: environment: - sdk: ">=2.17.3 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=1.17.0" dependencies: From 085cc73ec544dbb72f99f1aaf06eb33cb63f5f69 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 13 Jul 2022 19:36:40 +0800 Subject: [PATCH 020/121] chore: delete unused code --- .../app_flowy/packages/flowy_editor/example/pubspec.lock | 2 +- .../app_flowy/packages/flowy_editor/example/pubspec.yaml | 2 +- .../app_flowy/packages/flowy_editor/lib/document/node.dart | 7 ------- .../packages/flowy_editor/lib/document/state_tree.dart | 6 ------ 4 files changed, 2 insertions(+), 15 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock index 63ade5e65c..1420c9b23d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock @@ -185,5 +185,5 @@ packages: source: hosted version: "2.1.2" sdks: - dart: ">=2.17.3 <3.0.0" + dart: ">=2.17.0 <3.0.0" flutter: ">=1.17.0" diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml index 1a788cfb7d..d514607eac 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.17.3 <3.0.0" + sdk: ">=2.17.0 <3.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 1e951f4d79..ad49d9c8a2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -84,13 +84,6 @@ class Node extends ChangeNotifier with LinkedListEntry { return childAtIndex(path.first)?.childAtPath(path.sublist(1)); } - Node root() { - if (parent != null) { - return parent!.root(); - } - return this; - } - Path path([Path previous = const []]) { if (parent == null) { return previous; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 8f7f99aa13..af343f54a0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -16,12 +16,6 @@ class StateTree { return StateTree(root: root); } - // Path pathForNode(Node node) { - // var nodeRoot = node.root(); - // assert(nodeRoot == root, "Every node's root must be same as root"); - - // } - Node? nodeAtPath(Path path) { return root.childAtPath(path); } From d07854ebb4891d886029aebe086be737008d8b87 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 13 Jul 2022 20:28:07 +0800 Subject: [PATCH 021/121] chore: update image node widget example --- .../example/lib/plugin/image_node_widget.dart | 105 +++++++++--------- 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 0f2031c411..11607d7bd6 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -8,68 +8,71 @@ class ImageNodeBuilder extends NodeWidgetBuilder { required super.editorState, }) : super.create(); + @override + Widget build(BuildContext buildContext) { + return _ImageNodeWidget( + node: node, + editorState: editorState, + ); + } +} + +class _ImageNodeWidget extends StatelessWidget { + final Node node; + final EditorState editorState; + + const _ImageNodeWidget({ + Key? key, + required this.node, + required this.editorState, + }) : super(key: key); + String get src => node.attributes['image_src'] as String; @override - Widget build(BuildContext buildContext) { - // Future.delayed(const Duration(seconds: 5), () { - // node.updateAttributes({ - // 'image_src': - // "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400" - // }); - // }); + Widget build(BuildContext context) { return GestureDetector( child: ChangeNotifierProvider.value( value: node, - builder: (context, child) { - return Consumer( - builder: (context, value, child) { - return _build(context); - }, - ); - }, + builder: (_, __) => Consumer( + builder: ((context, value, child) => _build(context)), + ), ), onTap: () { - const newImageSrc = - "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400"; - final newAttribute = Attributes.from(node.attributes) - ..update( - 'image_src', - (value) => newImageSrc, - ); - editorState.update(node, newAttribute); + editorState.update( + node, + Attributes.from(node.attributes) + ..addAll( + { + 'image_src': + "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400", + }, + ), + ); }, ); } - Widget _build(BuildContext buildContext) { - final image = Image.network(src); - Widget? children; - if (node.children.isNotEmpty) { - children = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: buildContext, - node: e, - editorState: editorState, - ), - ), - ) - .toList(), - ); - } - if (children != null) { - return Column( - children: [ - image, - children, - ], - ); - } else { - return image; - } + Widget _build(BuildContext context) { + return Column( + children: [ + Image.network(src), + if (node.children.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: e, + editorState: editorState, + ), + ), + ) + .toList(), + ), + ], + ); } } From 00c628437d73ee7327672d7028a5c37612cfd95e Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 13 Jul 2022 23:08:41 +0800 Subject: [PATCH 022/121] chore: (draft) support text node widget editing --- .../example/lib/plugin/text_node_widget.dart | 176 ++++++++++++++---- .../flowy_editor/lib/document/node.dart | 1 + 2 files changed, 142 insertions(+), 35 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 482e6855f3..66ed5e3779 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; class TextNodeBuilder extends NodeWidgetBuilder { TextNodeBuilder.create({ @@ -11,41 +13,7 @@ class TextNodeBuilder extends NodeWidgetBuilder { @override Widget build(BuildContext buildContext) { - final richText = SelectableText.rich( - TextSpan( - text: node.attributes['content'] as String, - style: node.attributes.toTextStyle(), - ), - ); - - Widget? children; - if (node.children.isNotEmpty) { - children = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: buildContext, - node: e, - editorState: editorState, - ), - ), - ) - .toList(), - ); - } - - if (children != null) { - return Column( - children: [ - richText, - children, - ], - ); - } else { - return richText; - } + return _TextNodeWidget(node: node, editorState: editorState); } } @@ -57,3 +25,141 @@ extension on Attributes { ); } } + +class _TextNodeWidget extends StatefulWidget { + final Node node; + final EditorState editorState; + + const _TextNodeWidget({ + Key? key, + required this.node, + required this.editorState, + }) : super(key: key); + + @override + State<_TextNodeWidget> createState() => __TextNodeWidgetState(); +} + +class __TextNodeWidgetState extends State<_TextNodeWidget> + implements TextInputClient { + Node get node => widget.node; + EditorState get editorState => widget.editorState; + String get content => node.attributes['content'] as String; + TextEditingValue get textEditingValue => TextEditingValue(text: content); + + TextInputConnection? _textInputConnection; + + @override + Widget build(BuildContext context) { + final editableRichText = ChangeNotifierProvider.value( + value: node, + builder: (_, __) => Consumer( + builder: ((context, value, child) => SelectableText.rich( + TextSpan( + text: content, + style: node.attributes.toTextStyle(), + ), + onTap: () { + _textInputConnection?.close(); + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + enableDeltaModel: false, + inputType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + ), + ); + _textInputConnection + ?..show() + ..setEditingState(textEditingValue); + }, + )), + ), + ); + + final child = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + editableRichText, + if (node.children.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: e, + editorState: editorState, + ), + ), + ) + .toList(), + ), + ], + ); + return child; + } + + @override + void connectionClosed() { + // TODO: implement connectionClosed + } + + @override + // TODO: implement currentAutofillScope + AutofillScope? get currentAutofillScope => throw UnimplementedError(); + + @override + // TODO: implement currentTextEditingValue + TextEditingValue? get currentTextEditingValue => textEditingValue; + + @override + void insertTextPlaceholder(Size size) { + // TODO: implement insertTextPlaceholder + } + + @override + void performAction(TextInputAction action) { + // TODO: implement performAction + } + + @override + void performPrivateCommand(String action, Map data) { + // TODO: implement performPrivateCommand + } + + @override + void removeTextPlaceholder() { + // TODO: implement removeTextPlaceholder + } + + @override + void showAutocorrectionPromptRect(int start, int end) { + // TODO: implement showAutocorrectionPromptRect + } + + @override + void showToolbar() { + // TODO: implement showToolbar + } + + @override + void updateEditingValue(TextEditingValue value) { + debugPrint(value.text); + editorState.update( + node, + Attributes.from(node.attributes) + ..addAll( + { + 'content': value.text, + }, + ), + ); + } + + @override + void updateFloatingCursor(RawFloatingCursorPoint point) { + // TODO: implement updateFloatingCursor + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index ad49d9c8a2..9eccdb7897 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -29,6 +29,7 @@ class Node extends ChangeNotifier with LinkedListEntry { factory Node.fromJson(Map json) { assert(json['type'] is String); + // TODO: check the type that not exist on plugins. final jType = json['type'] as String; final jChildren = json['children'] as List?; final jAttributes = json['attributes'] != null From 6eb347a096077119bed5438bb70a9dfcc999f81b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 14 Jul 2022 13:59:05 +0800 Subject: [PATCH 023/121] feat: Add node validator and update op methods --- .../flowy_editor/example/lib/main.dart | 50 +++++++---- .../example/lib/plugin/image_node_widget.dart | 14 +--- .../example/lib/plugin/text_node_widget.dart | 82 ++++++++----------- .../flowy_editor/lib/document/node.dart | 36 ++++---- .../flowy_editor/lib/editor_state.dart | 21 +++-- .../lib/render/node_widget_builder.dart | 14 +++- .../flowy_editor/test/flowy_editor_test.dart | 2 +- 7 files changed, 120 insertions(+), 99 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 593cfa2871..4c1cd079b1 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -31,7 +31,7 @@ class MyApp extends StatelessWidget { // is not restarted. primarySwatch: Colors.blue, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + home: const MyHomePage(title: 'FlowyEditor Example'), ); } } @@ -83,23 +83,37 @@ class _MyHomePageState extends State { // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), - body: FutureBuilder( - future: rootBundle.loadString('assets/document.json'), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - final data = Map.from(json.decode(snapshot.data!)); - final document = StateTree.fromJson(data); - final editorState = EditorState( - document: document, - renderPlugins: renderPlugins, - ); - return editorState.build(context); - } - }, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FutureBuilder( + future: rootBundle.loadString('assets/document.json'), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + final data = + Map.from(json.decode(snapshot.data!)); + final document = StateTree.fromJson(data); + print(document.root.toString()); + final editorState = EditorState( + document: document, + renderPlugins: renderPlugins, + ); + return editorState.build(context); + } + }, + ), + SizedBox( + height: 50, + width: MediaQuery.of(context).size.width, + child: Container( + color: Colors.red, + ), + ) + ], ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 11607d7bd6..ac067cba6f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -39,16 +39,10 @@ class _ImageNodeWidget extends StatelessWidget { ), ), onTap: () { - editorState.update( - node, - Attributes.from(node.attributes) - ..addAll( - { - 'image_src': - "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400", - }, - ), - ); + editorState.update(node, { + 'image_src': + "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400" + }); }, ); } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 66ed5e3779..9c05688634 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -7,7 +7,11 @@ class TextNodeBuilder extends NodeWidgetBuilder { TextNodeBuilder.create({ required super.node, required super.editorState, - }) : super.create(); + }) : super.create() { + nodeValidator = ((node) { + return node.type == 'text' && node.attributes.containsKey('content'); + }); + } String get content => node.attributes['content'] as String; @@ -51,41 +55,35 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override Widget build(BuildContext context) { - final editableRichText = ChangeNotifierProvider.value( + return ChangeNotifierProvider.value( value: node, builder: (_, __) => Consumer( - builder: ((context, value, child) => SelectableText.rich( - TextSpan( - text: content, - style: node.attributes.toTextStyle(), - ), - onTap: () { - _textInputConnection?.close(); - _textInputConnection = TextInput.attach( - this, - const TextInputConfiguration( - enableDeltaModel: false, - inputType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - ), - ); - _textInputConnection - ?..show() - ..setEditingState(textEditingValue); - }, - )), - ), - ); - - final child = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - editableRichText, - if (node.children.isNotEmpty) - Column( + builder: ((context, value, child) { + return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( + children: [ + SelectableText.rich( + TextSpan( + text: content, + style: node.attributes.toTextStyle(), + ), + onTap: () { + _textInputConnection?.close(); + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + enableDeltaModel: false, + inputType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + ), + ); + _textInputConnection + ?..show() + ..setEditingState(textEditingValue); + }, + ), + if (node.children.isNotEmpty) + ...node.children.map( (e) => editorState.renderPlugins.buildWidget( context: NodeWidgetContext( buildContext: context, @@ -94,11 +92,11 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> ), ), ) - .toList(), - ), - ], + ], + ); + }), + ), ); - return child; } @override @@ -147,15 +145,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override void updateEditingValue(TextEditingValue value) { debugPrint(value.text); - editorState.update( - node, - Attributes.from(node.attributes) - ..addAll( - { - 'content': value.text, - }, - ), - ); + editorState.update(node, {'content': value.text}); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 9eccdb7897..0747416335 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -19,6 +19,8 @@ class Node extends ChangeNotifier with LinkedListEntry { return null; } + Path get path => _path(); + Node({ required this.type, required this.children, @@ -66,7 +68,7 @@ class Node extends ChangeNotifier with LinkedListEntry { } // Notify the new attributes - notifyListeners(); + parent?.notifyListeners(); } Node? childAtIndex(int index) { @@ -85,20 +87,6 @@ class Node extends ChangeNotifier with LinkedListEntry { return childAtIndex(path.first)?.childAtPath(path.sublist(1)); } - Path path([Path previous = const []]) { - if (parent == null) { - return previous; - } - var index = 0; - for (var child in parent!.children) { - if (child == this) { - break; - } - index += 1; - } - return parent!.path([index, ...previous]); - } - @override void insertAfter(Node entry) { entry.parent = parent; @@ -119,8 +107,10 @@ class Node extends ChangeNotifier with LinkedListEntry { @override void unlink() { - parent = null; super.unlink(); + + parent?.notifyListeners(); + parent = null; } Map toJson() { @@ -135,4 +125,18 @@ class Node extends ChangeNotifier with LinkedListEntry { } return map; } + + Path _path([Path previous = const []]) { + if (parent == null) { + return previous; + } + var index = 0; + for (var child in parent!.children) { + if (child == this) { + break; + } + index += 1; + } + return parent!._path([index, ...previous]); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index d60c06a497..ccbe3ebdbc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -36,18 +36,25 @@ class EditorState { } // TODO: move to a better place. - void update( - Node node, - Attributes attributes, - ) { + void update(Node node, Attributes attributes) { _applyOperation(UpdateOperation( - path: node.path(), - attributes: attributes, + path: node.path, + attributes: Attributes.from(attributes)..addAll(attributes), oldAttributes: node.attributes, )); } - _applyOperation(Operation op) { + // TODO: move to a better place. + void delete(Node node) { + _applyOperation( + DeleteOperation( + path: node.path, + removedValue: node, + ), + ); + } + + void _applyOperation(Operation op) { if (op is InsertOperation) { document.insert(op.path, op.value); } else if (op is UpdateOperation) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index 0dad14f821..4fb99419ce 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -3,9 +3,12 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flutter/material.dart'; +typedef NodeValidator = bool Function(T node); + class NodeWidgetBuilder { final EditorState editorState; final T node; + NodeValidator? nodeValidator; RenderPlugins get renderPlugins => editorState.renderPlugins; @@ -18,5 +21,14 @@ class NodeWidgetBuilder { /// and the layout style of [Node.Children]. Widget build(BuildContext buildContext) => throw UnimplementedError(); - Widget call(BuildContext buildContext) => build(buildContext); + Widget call(BuildContext buildContext) { + /// TODO: Validate the node + /// if failed, stop call build function, + /// return Empty widget, and throw Error. + if (nodeValidator != null && nodeValidator!(node) != true) { + throw Exception( + 'Node validate failure, node = { type: ${node.type}, attributes: ${node.attributes} }'); + } + return build(buildContext); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index 6635f46827..d272364b44 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -39,7 +39,7 @@ void main() { expect(checkBoxNode != null, true); final textType = checkBoxNode!.attributes['text-type']; expect(textType != null, true); - final path = checkBoxNode.path([]); + final path = checkBoxNode.path; expect(pathEquals(path, [1, 0]), true); }); From 3cbac6f3f918b9b0b517d5471e35ef5e9704323e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 13 Jul 2022 17:38:05 +0800 Subject: [PATCH 024/121] feat: invert delta --- .../example/lib/plugin/text_node_widget.dart | 1 + .../flowy_editor/lib/document/attributes.dart | 23 +++++++++++ .../flowy_editor/lib/document/node.dart | 3 +- .../flowy_editor/lib/document/state_tree.dart | 1 + .../flowy_editor/lib/document/text_delta.dart | 40 ++++++++++++++----- .../flowy_editor/lib/operation/operation.dart | 17 ++++++++ 6 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 66ed5e3779..1af9c7ee7e 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:flowy_editor/document/attributes.dart'; class TextNodeBuilder extends NodeWidgetBuilder { TextNodeBuilder.create({ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart new file mode 100644 index 0000000000..ea7106ab0a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart @@ -0,0 +1,23 @@ +typedef Attributes = Map; + +int hashAttributes(Attributes attributes) { + return Object.hashAllUnordered( + attributes.entries.map((e) => Object.hash(e.key, e.value))); +} + +Attributes invertAttributes(Attributes? attr, Attributes? base) { + attr ??= {}; + base ??= {}; + final Attributes baseInverted = base.keys.fold({}, (memo, key) { + if (base![key] != attr![key] && attr.containsKey(key)) { + memo[key] = base[key]; + } + return memo; + }); + return attr.keys.fold(baseInverted, (memo, key) { + if (attr![key] != base![key] && base.containsKey(key)) { + memo[key] = null; + } + return memo; + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 9eccdb7897..84493de0f9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,8 +1,7 @@ import 'dart:collection'; import 'package:flowy_editor/document/path.dart'; import 'package:flutter/material.dart'; - -typedef Attributes = Map; +import './attributes.dart'; class Node extends ChangeNotifier with LinkedListEntry { Node? parent; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index af343f54a0..6fe58d2886 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -1,5 +1,6 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/path.dart'; +import './attributes.dart'; class StateTree { final Node root; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index a52a96426a..2d3f8301f5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import './node.dart'; +import './attributes.dart'; // constant number: 2^53 - 1 const int _maxInt = 9007199254740991; @@ -22,14 +22,6 @@ class TextOperation { } } -int _hashAttributes(Attributes attributes) { - return Object.hashAllUnordered( - attributes.entries.map( - (e) => Object.hash(e.key, e.value), - ), - ); -} - class TextInsert extends TextOperation { String content; final Attributes? _attributes; @@ -60,7 +52,7 @@ class TextInsert extends TextOperation { final contentHash = content.hashCode; final attrs = _attributes; return Object.hash( - contentHash, attrs == null ? null : _hashAttributes(attrs)); + contentHash, attrs == null ? null : hashAttributes(attrs)); } } @@ -104,7 +96,7 @@ class TextRetain extends TextOperation { @override int get hashCode { final attrs = _attributes; - return Object.hash(_length, attrs == null ? null : _hashAttributes(attrs)); + return Object.hash(_length, attrs == null ? null : hashAttributes(attrs)); } } @@ -404,6 +396,32 @@ class Delta { int get hashCode { return hashList(operations); } + + Delta invert(Delta base) { + final inverted = Delta(); + operations.fold(0, (int previousValue, op) { + if (op is TextInsert) { + inverted.delete(op.length); + } else if (op is TextRetain && op.attributes == null) { + inverted.retain(op.length); + return previousValue + op.length; + } else if (op is TextDelete || op is TextRetain) { + final length = op.length; + final slice = base.slice(previousValue, previousValue + length); + for (final baseOp in slice.operations) { + if (op is TextDelete) { + inverted.add(baseOp); + } else if (op is TextRetain && op.attributes != null) { + inverted.retain(baseOp.length, + invertAttributes(op.attributes, baseOp.attributes)); + } + } + return previousValue + length; + } + return previousValue; + }); + return inverted.chop(); + } } Attributes? _composeMap(Attributes? a, Attributes? b) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index 4ca7b9ca83..5fb9a523a8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -1,5 +1,7 @@ import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/document/attributes.dart'; abstract class Operation { Operation invert(); @@ -61,3 +63,18 @@ class DeleteOperation extends Operation { ); } } + +class TextEditOperation extends Operation { + final Path path; + final Delta delta; + + TextEditOperation({ + required this.path, + required this.delta, + }); + + @override + Operation invert() { + return TextEditOperation(path: path, delta: delta); + } +} From def03273b85065ba5cf51e6d7aa3fe684f25e7dd Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 13 Jul 2022 18:46:00 +0800 Subject: [PATCH 025/121] test: invert --- .../example/lib/plugin/image_node_widget.dart | 1 + .../flowy_editor/lib/document/attributes.dart | 19 + .../flowy_editor/lib/document/text_delta.dart | 22 +- .../flowy_editor/lib/editor_state.dart | 1 + .../flowy_editor/test/delta_test.dart | 339 ++++++++++-------- 5 files changed, 206 insertions(+), 176 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 11607d7bd6..33425eccd5 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,6 +1,7 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:flowy_editor/document/attributes.dart'; class ImageNodeBuilder extends NodeWidgetBuilder { ImageNodeBuilder.create({ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart index ea7106ab0a..6e845420ef 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart @@ -21,3 +21,22 @@ Attributes invertAttributes(Attributes? attr, Attributes? base) { return memo; }); } + +Attributes? composeAttributes(Attributes? a, Attributes? b) { + a ??= {}; + b ??= {}; + final Attributes attributes = {}; + attributes.addAll(b); + + for (final entry in a.entries) { + if (!b.containsKey(entry.key)) { + attributes[entry.key] = entry.value; + } + } + + if (attributes.isEmpty) { + return null; + } + + return attributes; +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index 2d3f8301f5..bbf8e20a68 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -336,7 +336,8 @@ class Delta { final length = min(thisIter.peekLength(), otherIter.peekLength()); final thisOp = thisIter.next(length); final otherOp = otherIter.next(length); - final attributes = _composeMap(thisOp.attributes, otherOp.attributes); + final attributes = + composeAttributes(thisOp.attributes, otherOp.attributes); if (otherOp is TextRetain && otherOp.length > 0) { TextOperation? newOp; if (thisOp is TextRetain) { @@ -423,22 +424,3 @@ class Delta { return inverted.chop(); } } - -Attributes? _composeMap(Attributes? a, Attributes? b) { - a ??= {}; - b ??= {}; - final Attributes attributes = {}; - attributes.addAll(b); - - for (final entry in a.entries) { - if (!b.containsKey(entry.key)) { - attributes[entry.key] = entry.value; - } - } - - if (attributes.isEmpty) { - return null; - } - - return attributes; -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index d60c06a497..30ffb68009 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,5 +1,6 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/document/attributes.dart'; import 'package:flutter/material.dart'; import './document/state_tree.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index 3ddd01efb7..bce2517744 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -2,172 +2,199 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flowy_editor/document/text_delta.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - test('test delta', () { - final delta = Delta([ - TextInsert('Gandalf', { + group('compose', () { + test('test delta', () { + final delta = Delta([ + TextInsert('Gandalf', { + 'bold': true, + }), + TextInsert(' the '), + TextInsert('Grey', { + 'color': '#ccc', + }) + ]); + + final death = Delta().retain(12).insert("White", { + 'color': '#fff', + }).delete(4); + + final restores = delta.compose(death); + expect(restores.operations, [ + TextInsert('Gandalf', {'bold': true}), + TextInsert(' the '), + TextInsert('White', {'color': '#fff'}), + ]); + }); + test('compose()', () { + final a = Delta().insert('A'); + final b = Delta().insert('B'); + final expected = Delta().insert('B').insert('A'); + expect(a.compose(b), expected); + }); + test('insert + retain', () { + final a = Delta().insert('A'); + final b = Delta().retain(1, { 'bold': true, - }), - TextInsert(' the '), - TextInsert('Grey', { - 'color': '#ccc', - }) - ]); - - final death = Delta().retain(12).insert("White", { - 'color': '#fff', - }).delete(4); - - final restores = delta.compose(death); - expect(restores.operations, [ - TextInsert('Gandalf', {'bold': true}), - TextInsert(' the '), - TextInsert('White', {'color': '#fff'}), - ]); - }); - test('compose()', () { - final a = Delta().insert('A'); - final b = Delta().insert('B'); - final expected = Delta().insert('B').insert('A'); - expect(a.compose(b), expected); - }); - test('insert + retain', () { - final a = Delta().insert('A'); - final b = Delta().retain(1, { - 'bold': true, - 'color': 'red', + 'color': 'red', + }); + final expected = Delta().insert('A', { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); }); - final expected = Delta().insert('A', { - 'bold': true, - 'color': 'red', + test('insert + delete', () { + final a = Delta().insert('A'); + final b = Delta().delete(1); + final expected = Delta(); + expect(a.compose(b), expected); }); - expect(a.compose(b), expected); - }); - test('insert + delete', () { - final a = Delta().insert('A'); - final b = Delta().delete(1); - final expected = Delta(); - expect(a.compose(b), expected); - }); - test('delete + insert', () { - final a = Delta().delete(1); - final b = Delta().insert('B'); - final expected = Delta().insert('B').delete(1); - expect(a.compose(b), expected); - }); - test('delete + retain', () { - final a = Delta().delete(1); - final b = Delta().retain(1, { - 'bold': true, - 'color': 'red', + test('delete + insert', () { + final a = Delta().delete(1); + final b = Delta().insert('B'); + final expected = Delta().insert('B').delete(1); + expect(a.compose(b), expected); }); - final expected = Delta().delete(1).retain(1, { - 'bold': true, - 'color': 'red', + test('delete + retain', () { + final a = Delta().delete(1); + final b = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta().delete(1).retain(1, { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); }); - expect(a.compose(b), expected); - }); - test('delete + delete', () { - final a = Delta().delete(1); - final b = Delta().delete(1); - final expected = Delta().delete(2); - expect(a.compose(b), expected); - }); - test('retain + insert', () { - final a = Delta().retain(1, {'color': 'blue'}); - final b = Delta().insert('B'); - final expected = Delta().insert('B').retain(1, { - 'color': 'blue', + test('delete + delete', () { + final a = Delta().delete(1); + final b = Delta().delete(1); + final expected = Delta().delete(2); + expect(a.compose(b), expected); }); - expect(a.compose(b), expected); - }); - test('retain + retain', () { - final a = Delta().retain(1, { - 'color': 'blue', + test('retain + insert', () { + final a = Delta().retain(1, {'color': 'blue'}); + final b = Delta().insert('B'); + final expected = Delta().insert('B').retain(1, { + 'color': 'blue', + }); + expect(a.compose(b), expected); }); - final b = Delta().retain(1, { - 'bold': true, - 'color': 'red', + test('retain + retain', () { + final a = Delta().retain(1, { + 'color': 'blue', + }); + final b = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); }); - final expected = Delta().retain(1, { - 'bold': true, - 'color': 'red', + test('retain + delete', () { + final a = Delta().retain(1, { + 'color': 'blue', + }); + final b = Delta().delete(1); + final expected = Delta().delete(1); + expect(a.compose(b), expected); }); - expect(a.compose(b), expected); - }); - test('retain + delete', () { - final a = Delta().retain(1, { - 'color': 'blue', + test('insert in middle of text', () { + final a = Delta().insert('Hello'); + final b = Delta().retain(3).insert('X'); + final expected = Delta().insert('HelXlo'); + expect(a.compose(b), expected); + }); + test('insert and delete ordering', () { + final a = Delta().insert('Hello'); + final b = Delta().insert('Hello'); + final insertFirst = Delta().retain(3).insert('X').delete(1); + final deleteFirst = Delta().retain(3).delete(1).insert('X'); + final expected = Delta().insert('HelXo'); + expect(a.compose(insertFirst), expected); + expect(b.compose(deleteFirst), expected); + }); + test('delete entire text', () { + final a = Delta().retain(4).insert('Hello'); + final b = Delta().delete(9); + final expected = Delta().delete(4); + expect(a.compose(b), expected); + }); + test('retain more than length of text', () { + final a = Delta().insert('Hello'); + final b = Delta().retain(10); + final expected = Delta().insert('Hello'); + expect(a.compose(b), expected); + }); + test('retain start optimization', () { + final a = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}) + .delete(1); + final b = Delta().retain(3).insert('D'); + final expected = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}) + .insert('D') + .delete(1); + expect(a.compose(b), expected); + }); + test('retain end optimization', () { + final a = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}); + final b = Delta().delete(1); + final expected = Delta().insert('B').insert('C', {'bold': true}); + expect(a.compose(b), expected); + }); + test('retain end optimization join', () { + final a = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}) + .insert('D') + .insert('E', {'bold': true}) + .insert('F'); + final b = Delta().retain(1).delete(1); + final expected = Delta() + .insert('AC', {'bold': true}) + .insert('D') + .insert('E', {'bold': true}) + .insert('F'); + expect(a.compose(b), expected); }); - final b = Delta().delete(1); - final expected = Delta().delete(1); - expect(a.compose(b), expected); }); - test('insert in middle of text', () { - final a = Delta().insert('Hello'); - final b = Delta().retain(3).insert('X'); - final expected = Delta().insert('HelXlo'); - expect(a.compose(b), expected); - }); - test('insert and delete ordering', () { - final a = Delta().insert('Hello'); - final b = Delta().insert('Hello'); - final insertFirst = Delta().retain(3).insert('X').delete(1); - final deleteFirst = Delta().retain(3).delete(1).insert('X'); - final expected = Delta().insert('HelXo'); - expect(a.compose(insertFirst), expected); - expect(b.compose(deleteFirst), expected); - }); - test('delete entire text', () { - final a = Delta().retain(4).insert('Hello'); - final b = Delta().delete(9); - final expected = Delta().delete(4); - expect(a.compose(b), expected); - }); - test('retain more than length of text', () { - final a = Delta().insert('Hello'); - final b = Delta().retain(10); - final expected = Delta().insert('Hello'); - expect(a.compose(b), expected); - }); - test('retain start optimization', () { - final a = Delta() - .insert('A', {'bold': true}) - .insert('B') - .insert('C', {'bold': true}) - .delete(1); - final b = Delta().retain(3).insert('D'); - final expected = Delta() - .insert('A', {'bold': true}) - .insert('B') - .insert('C', {'bold': true}) - .insert('D') - .delete(1); - expect(a.compose(b), expected); - }); - test('retain end optimization', () { - final a = Delta() - .insert('A', {'bold': true}) - .insert('B') - .insert('C', {'bold': true}); - final b = Delta().delete(1); - final expected = Delta().insert('B').insert('C', {'bold': true}); - expect(a.compose(b), expected); - }); - test('retain end optimization join', () { - final a = Delta() - .insert('A', {'bold': true}) - .insert('B') - .insert('C', {'bold': true}) - .insert('D') - .insert('E', {'bold': true}) - .insert('F'); - final b = Delta().retain(1).delete(1); - final expected = Delta() - .insert('AC', {'bold': true}) - .insert('D') - .insert('E', {'bold': true}) - .insert('F'); - expect(a.compose(b), expected); + group('invert', () { + test('insert', () { + final delta = Delta().retain(2).insert('A'); + final base = Delta().insert('12346'); + final expected = Delta().retain(2).delete(1); + final inverted = delta.invert(base); + expect(expected, inverted); + expect(base.compose(delta).compose(inverted), base); + }); + test('delete', () { + final delta = Delta().retain(2).delete(3); + final base = Delta().insert('123456'); + final expected = Delta().retain(2).insert('345'); + final inverted = delta.invert(base); + expect(expected, inverted); + expect(base.compose(delta).compose(inverted), base); + }); + // test('retain', () { + // final delta = Delta().retain(2).retain(3, {'bold': true}); + // final base = Delta().insert('123456'); + // final expected = Delta().retain(2).retain(3, {'bold': null}); + // final inverted = delta.invert(base); + // expect(expected, inverted); + // expect(base.compose(delta).compose(inverted), base); + // }); }); } From 1039c5517fd02fa0aec6426af30dc0501b384c86 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 16 Jul 2022 20:08:41 +0800 Subject: [PATCH 026/121] fix: rebuilding node widgets when the subtype changes --- .../packages/flowy_editor/lib/document/node.dart | 13 +++++++++---- .../flowy_editor/lib/document/state_tree.dart | 2 +- .../packages/flowy_editor/lib/editor_state.dart | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 0747416335..55e96451b1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -13,8 +13,9 @@ class Node extends ChangeNotifier with LinkedListEntry { String? get subtype { // TODO: make 'subtype' as a const value. if (attributes.containsKey('subtype')) { - assert(attributes['subtype'] is String, 'subtype must be a [String]'); - return attributes['subtype'] as String; + assert(attributes['subtype'] is String?, + 'subtype must be a [String] or [null]'); + return attributes['subtype'] as String?; } return null; } @@ -63,12 +64,16 @@ class Node extends ChangeNotifier with LinkedListEntry { } void updateAttributes(Attributes attributes) { + bool shouldNotifyParent = + this.attributes['subtype'] != attributes['subtype']; + for (final attribute in attributes.entries) { this.attributes[attribute.key] = attribute.value; } - // Notify the new attributes - parent?.notifyListeners(); + // if attributes contains 'subtype', should notify parent to rebuild node + // else, just notify current node. + shouldNotifyParent ? parent?.notifyListeners() : notifyListeners(); } Node? childAtIndex(int index) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index af343f54a0..01ab07381e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -51,7 +51,7 @@ class StateTree { if (updatedNode == null) { return null; } - final previousAttributes = {...updatedNode.attributes}; + final previousAttributes = Attributes.from(updatedNode.attributes); updatedNode.updateAttributes(attributes); return previousAttributes; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index ccbe3ebdbc..0199bfe337 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -39,7 +39,7 @@ class EditorState { void update(Node node, Attributes attributes) { _applyOperation(UpdateOperation( path: node.path, - attributes: Attributes.from(attributes)..addAll(attributes), + attributes: Attributes.from(node.attributes)..addAll(attributes), oldAttributes: node.attributes, )); } From de06188c5e441f282861358f7e326e80ee8525ca Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 18 Jul 2022 10:41:58 +0800 Subject: [PATCH 027/121] feat: rebuild the widget when node changed. --- .../flowy_editor/example/assets/document.json | 6 +- .../flowy_editor/example/lib/main.dart | 66 ++++++----------- .../lib/plugin/document_node_widget.dart | 50 +++++++++++++ .../example/lib/plugin/image_node_widget.dart | 9 +-- .../example/lib/plugin/text_node_widget.dart | 74 +++++++++---------- .../lib/render/node_widget_builder.dart | 23 +++++- .../packages/flowy_editor/pubspec.yaml | 2 + 7 files changed, 133 insertions(+), 97 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 2ddedf70b3..bec451fba4 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -1,9 +1,7 @@ { "document": { - "type": "text", - "attributes": { - "content": "TITLE" - }, + "type": "editor", + "attributes": {}, "children": [ { "type": "text", diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 4c1cd079b1..cc9acce929 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:example/plugin/document_node_widget.dart'; import 'package:example/plugin/image_node_widget.dart'; import 'package:example/plugin/text_node_widget.dart'; import 'package:example/plugin/text_with_check_box_node_widget.dart'; @@ -56,23 +57,16 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { final RenderPlugins renderPlugins = RenderPlugins(); + late EditorState _editorState; @override void initState() { super.initState(); renderPlugins - ..register( - 'text', - TextNodeBuilder.create, - ) - ..register( - 'image', - ImageNodeBuilder.create, - ) - ..register( - 'text/with-checkbox', - TextWithCheckBoxNodeBuilder.create, - ); + ..register('editor', EditorNodeWidgetBuilder.create) + ..register('text', TextNodeBuilder.create) + ..register('image', ImageNodeBuilder.create) + ..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create); } @override @@ -83,37 +77,23 @@ class _MyHomePageState extends State { // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FutureBuilder( - future: rootBundle.loadString('assets/document.json'), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - final data = - Map.from(json.decode(snapshot.data!)); - final document = StateTree.fromJson(data); - print(document.root.toString()); - final editorState = EditorState( - document: document, - renderPlugins: renderPlugins, - ); - return editorState.build(context); - } - }, - ), - SizedBox( - height: 50, - width: MediaQuery.of(context).size.width, - child: Container( - color: Colors.red, - ), - ) - ], + body: FutureBuilder( + future: rootBundle.loadString('assets/document.json'), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + final data = Map.from(json.decode(snapshot.data!)); + final document = StateTree.fromJson(data); + _editorState = EditorState( + document: document, + renderPlugins: renderPlugins, + ); + return _editorState.build(context); + } + }, ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart new file mode 100644 index 0000000000..2de62948d5 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart @@ -0,0 +1,50 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; + +class EditorNodeWidgetBuilder extends NodeWidgetBuilder { + EditorNodeWidgetBuilder.create({ + required super.editorState, + required super.node, + }) : super.create(); + + @override + Widget build(BuildContext buildContext) { + return SingleChildScrollView( + child: _EditorNodeWidget( + node: node, + editorState: editorState, + ), + ); + } +} + +class _EditorNodeWidget extends StatelessWidget { + final Node node; + final EditorState editorState; + + const _EditorNodeWidget({ + Key? key, + required this.node, + required this.editorState, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: e, + editorState: editorState, + ), + ), + ) + .toList(), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 5084b6333c..c48941011f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,7 +1,5 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flowy_editor/document/attributes.dart'; class ImageNodeBuilder extends NodeWidgetBuilder { ImageNodeBuilder.create({ @@ -33,12 +31,7 @@ class _ImageNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - child: ChangeNotifierProvider.value( - value: node, - builder: (_, __) => Consumer( - builder: ((context, value, child) => _build(context)), - ), - ), + child: _build(context), onTap: () { editorState.update(node, { 'image_src': diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 59e466c92e..a56327ead5 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; import 'package:flowy_editor/document/attributes.dart'; class TextNodeBuilder extends NodeWidgetBuilder { @@ -56,47 +55,40 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: node, - builder: (_, __) => Consumer( - builder: ((context, value, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText.rich( - TextSpan( - text: content, - style: node.attributes.toTextStyle(), - ), - onTap: () { - _textInputConnection?.close(); - _textInputConnection = TextInput.attach( - this, - const TextInputConfiguration( - enableDeltaModel: false, - inputType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - ), - ); - _textInputConnection - ?..show() - ..setEditingState(textEditingValue); - }, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText.rich( + TextSpan( + text: content, + style: node.attributes.toTextStyle(), + ), + onTap: () { + _textInputConnection?.close(); + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + enableDeltaModel: false, + inputType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, ), - if (node.children.isNotEmpty) - ...node.children.map( - (e) => editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: e, - editorState: editorState, - ), - ), - ) - ], - ); - }), - ), + ); + _textInputConnection + ?..show() + ..setEditingState(textEditingValue); + }, + ), + if (node.children.isNotEmpty) + ...node.children.map( + (e) => editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: e, + editorState: editorState, + ), + ), + ) + ], ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index 4fb99419ce..badce60694 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -2,12 +2,15 @@ import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; typedef NodeValidator = bool Function(T node); class NodeWidgetBuilder { final EditorState editorState; final T node; + + bool rebuildOnNodeChanged; NodeValidator? nodeValidator; RenderPlugins get renderPlugins => editorState.renderPlugins; @@ -15,6 +18,7 @@ class NodeWidgetBuilder { NodeWidgetBuilder.create({ required this.editorState, required this.node, + this.rebuildOnNodeChanged = true, }); /// Render the current [Node] @@ -29,6 +33,23 @@ class NodeWidgetBuilder { throw Exception( 'Node validate failure, node = { type: ${node.type}, attributes: ${node.attributes} }'); } - return build(buildContext); + + if (rebuildOnNodeChanged) { + return _buildNodeChangeNotifier(buildContext); + } else { + return build(buildContext); + } + } + + Widget _buildNodeChangeNotifier(BuildContext buildContext) { + return ChangeNotifierProvider.value( + value: node, + builder: (_, __) => Consumer( + builder: ((context, value, child) { + debugPrint('Node changed, and rebuilding...'); + return build(context); + }), + ), + ); } } diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 6a6d32d580..74ca437e27 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: flutter: sdk: flutter + provider: ^6.0.3 + dev_dependencies: flutter_test: sdk: flutter From 4b7c997083912c519fe6e4b941df6f11f720164b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 18 Jul 2022 15:49:39 +0800 Subject: [PATCH 028/121] feat: text delta to text span --- .../flowy_editor/example/assets/document.json | 27 ++++--- .../example/lib/plugin/text_node_widget.dart | 60 ++++++++++++++-- .../flowy_editor/lib/document/node.dart | 34 +++++++-- .../flowy_editor/lib/document/text_delta.dart | 70 ++++++++++++++----- .../flowy_editor/lib/document/text_node.dart | 13 ---- 5 files changed, 154 insertions(+), 50 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 2ddedf70b3..04a9e0cda8 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -7,39 +7,46 @@ "children": [ { "type": "text", + "delta": [ + { "insert": "With " }, + { "insert": "AppFlowy", "attributes": { "href": "https://www.appflowy.io/" } }, + { "insert": ", you can build detailed lists of to-do’s for different projects while tracking the status of each one" } + ], "attributes": { "subtype": "with-checkbox", "font-size": 30, - "content": "aaaaaaaaaaaaaaaaaaaaaaaa", "checkbox": false } }, { "type": "text", + "delta": [ + { "insert": "You can " }, + { "insert": "host", "attributes": { "italic": true } }, + { "insert": " " }, + { "insert": "AppFlowy", "attributes": { "bold": true } }, + { "insert": " " }, + { "insert": "wherever you want", "attributes": { "underline": true }}, + { "insert": "; no vendor lock-in." } + ], "attributes": { "subtype": "with-checkbox", "text-type": "heading1", "font-size": 30, - "content": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "checkbox": false } }, { "type": "text", + "delta": [{ "insert": "Design and modify AppFlowy your way with an open core codebase." }], "attributes": { "text-type": "heading1", - "font-size": 30, - "content": "cccccccccccccccccccccc" - } - }, - { - "type": "image", - "attributes": { - "image_src": "https://images.pexels.com/photos/12499889/pexels-photo-12499889.jpeg?fm=jpg&w=640&h=427" + "font-size": 30 } }, { "type": "text", + "delta": [{ "insert": "AppFlowy is built with Flutter and Rust. What does this mean? Faster development, better native experience, and more reliable performance." }], "attributes": { "text-type": "heading1", "font-size": 30, diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 59e466c92e..7ce67fcdf6 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -1,3 +1,5 @@ +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/services.dart'; @@ -10,7 +12,7 @@ class TextNodeBuilder extends NodeWidgetBuilder { required super.editorState, }) : super.create() { nodeValidator = ((node) { - return node.type == 'text' && node.attributes.containsKey('content'); + return node.type == 'text'; }); } @@ -31,6 +33,55 @@ extension on Attributes { } } +TextSpan _textInsertToTextSpan(TextInsert textInsert) { + FontWeight? fontWeight; + FontStyle? fontStyle; + TextDecoration? decoration; + GestureRecognizer? gestureRecognizer; + Color? color; + final attributes = textInsert.attributes; + if (attributes?['bold'] == true) { + fontWeight = FontWeight.bold; + } + if (attributes?['italic'] == true) { + fontStyle = FontStyle.italic; + } + if (attributes?["underline"] == true) { + decoration = TextDecoration.underline; + } + if (attributes?["href"] is String) { + color = const Color.fromARGB(255, 55, 120, 245); + decoration = TextDecoration.underline; + gestureRecognizer = TapGestureRecognizer() + ..onTap = () { + // TODO: open the link + }; + } + return TextSpan( + text: textInsert.content, + style: TextStyle( + fontWeight: fontWeight, + fontStyle: fontStyle, + decoration: decoration, + color: color, + ), + recognizer: gestureRecognizer); +} + +extension on TextNode { + List toTextSpans() { + final result = []; + + for (final op in delta.operations) { + if (op is TextInsert) { + result.add(_textInsertToTextSpan(op)); + } + } + + return result; + } +} + class _TextNodeWidget extends StatefulWidget { final Node node; final EditorState editorState; @@ -49,8 +100,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> implements TextInputClient { Node get node => widget.node; EditorState get editorState => widget.editorState; - String get content => node.attributes['content'] as String; - TextEditingValue get textEditingValue => TextEditingValue(text: content); + TextEditingValue get textEditingValue => const TextEditingValue(); TextInputConnection? _textInputConnection; @@ -60,13 +110,13 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> value: node, builder: (_, __) => Consumer( builder: ((context, value, child) { + final textNode = value as TextNode; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SelectableText.rich( TextSpan( - text: content, - style: node.attributes.toTextStyle(), + children: textNode.toTextSpans(), ), onTap: () { _textInputConnection?.close(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index cf706df856..ba4cb10525 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,5 +1,6 @@ import 'dart:collection'; import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/text_delta.dart'; import 'package:flutter/material.dart'; import './attributes.dart'; @@ -49,11 +50,23 @@ class Node extends ChangeNotifier with LinkedListEntry { ); } - final node = Node( - type: jType, - children: children, - attributes: jAttributes, - ); + Node node; + + if (jType == "text") { + final jDelta = json['delta'] as List?; + final delta = jDelta == null ? Delta() : Delta.fromJson(jDelta); + node = TextNode( + type: jType, + children: children, + attributes: jAttributes, + delta: delta); + } else { + node = Node( + type: jType, + children: children, + attributes: jAttributes, + ); + } for (final child in children) { child.parent = node; @@ -144,3 +157,14 @@ class Node extends ChangeNotifier with LinkedListEntry { return parent!._path([index, ...previous]); } } + +class TextNode extends Node { + final Delta delta; + + TextNode({ + required super.type, + required super.children, + required super.attributes, + required this.delta, + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index bbf8e20a68..b87ca71b95 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -60,10 +60,8 @@ class TextRetain extends TextOperation { int _length; final Attributes? _attributes; - TextRetain({ - required length, - attributes, - }) : _length = length, + TextRetain(length, [Attributes? attributes]) + : _length = length, _attributes = attributes; @override @@ -103,9 +101,7 @@ class TextRetain extends TextOperation { class TextDelete extends TextOperation { int _length; - TextDelete({ - required int length, - }) : _length = length; + TextDelete(int length) : _length = length; @override bool get isEmpty { @@ -167,7 +163,7 @@ class _OpIterator { length ??= _maxInt; if (_index >= _operations.length) { - return TextRetain(length: _maxInt); + return TextRetain(_maxInt); } final nextOp = _operations[_index]; @@ -182,15 +178,13 @@ class _OpIterator { _offset += length; } if (nextOp is TextDelete) { - return TextDelete( - length: length, - ); + return TextDelete(length); } if (nextOp is TextRetain) { return TextRetain( - length: length, - attributes: nextOp.attributes, + length, + nextOp.attributes, ); } @@ -201,7 +195,7 @@ class _OpIterator { ); } - return TextRetain(length: _maxInt); + return TextRetain(_maxInt); } List rest() { @@ -221,10 +215,52 @@ class _OpIterator { } } +Attributes? _attributesFromJSON(Map? json) { + if (json == null) { + return null; + } + final result = {}; + + for (final entry in json.entries) { + result[entry.key] = entry.value; + } + + return result; +} + +TextOperation? _textOperationFromJson(Map json) { + TextOperation? result; + + if (json['insert'] is String) { + result = TextInsert(json['insert'] as String, + _attributesFromJSON(json['attributes'] as Map?)); + } else if (json['retain'] is int) { + result = TextRetain(json['retain'] as int, + _attributesFromJSON(json['attributes'] as Map?)); + } else if (json['delete'] is int) { + result = TextDelete(json['delete'] as int); + } + + return result; +} + // basically copy from: https://github.com/quilljs/delta class Delta { final List operations; + factory Delta.fromJson(List list) { + final operations = []; + + for (final obj in list) { + final op = _textOperationFromJson(obj as Map); + if (op != null) { + operations.add(op); + } + } + + return Delta(operations); + } + Delta([List? ops]) : operations = ops ?? []; Delta add(TextOperation textOp) { @@ -288,12 +324,12 @@ class Delta { } Delta retain(int length, [Attributes? attributes]) { - final op = TextRetain(length: length, attributes: attributes); + final op = TextRetain(length, attributes); return add(op); } Delta delete(int length) { - final op = TextDelete(length: length); + final op = TextDelete(length); return add(op); } @@ -341,7 +377,7 @@ class Delta { if (otherOp is TextRetain && otherOp.length > 0) { TextOperation? newOp; if (thisOp is TextRetain) { - newOp = TextRetain(length: length, attributes: attributes); + newOp = TextRetain(length, attributes); } else if (thisOp is TextInsert) { newOp = TextInsert(thisOp.content, attributes); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart deleted file mode 100644 index 535431b615..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart +++ /dev/null @@ -1,13 +0,0 @@ -import './text_delta.dart'; -import './node.dart'; - -class TextNode extends Node { - final Delta delta; - - TextNode({ - required super.type, - required super.children, - required super.attributes, - required this.delta, - }); -} From d5015f0dfb848de9fa3e916d6e8680e2f14809fc Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 18 Jul 2022 17:44:37 +0800 Subject: [PATCH 029/121] feat: add url_launcher into dependency --- .../example/ios/Flutter/Debug.xcconfig | 1 + .../example/ios/Flutter/Release.xcconfig | 1 + .../packages/flowy_editor/example/ios/Podfile | 41 ++++++ .../example/lib/plugin/text_node_widget.dart | 120 +++++++++--------- .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + .../flowy_editor/example/macos/Podfile | 40 ++++++ .../flowy_editor/example/macos/Podfile.lock | 22 ++++ .../macos/Runner.xcodeproj/project.pbxproj | 62 ++++++++- .../contents.xcworkspacedata | 3 + .../flowy_editor/example/pubspec.lock | 77 ++++++++++- .../flowy_editor/example/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 17 files changed, 316 insertions(+), 65 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/ios/Podfile create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Podfile create mode 100644 frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig index 592ceee85b..ec97fc6f30 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig index 592ceee85b..c4855bfe20 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Podfile b/frontend/app_flowy/packages/flowy_editor/example/ios/Podfile new file mode 100644 index 0000000000..1e8c3c90a5 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 49dcfad8f4..a020ba11ca 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -3,7 +3,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/services.dart'; -import 'package:flowy_editor/document/attributes.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class TextNodeBuilder extends NodeWidgetBuilder { TextNodeBuilder.create({ @@ -15,72 +15,12 @@ class TextNodeBuilder extends NodeWidgetBuilder { }); } - String get content => node.attributes['content'] as String; - @override Widget build(BuildContext buildContext) { return _TextNodeWidget(node: node, editorState: editorState); } } -extension on Attributes { - TextStyle toTextStyle() { - return TextStyle( - color: this['color'] != null ? Colors.red : Colors.black, - fontSize: this['font-size'] != null ? 30 : 15, - ); - } -} - -TextSpan _textInsertToTextSpan(TextInsert textInsert) { - FontWeight? fontWeight; - FontStyle? fontStyle; - TextDecoration? decoration; - GestureRecognizer? gestureRecognizer; - Color? color; - final attributes = textInsert.attributes; - if (attributes?['bold'] == true) { - fontWeight = FontWeight.bold; - } - if (attributes?['italic'] == true) { - fontStyle = FontStyle.italic; - } - if (attributes?["underline"] == true) { - decoration = TextDecoration.underline; - } - if (attributes?["href"] is String) { - color = const Color.fromARGB(255, 55, 120, 245); - decoration = TextDecoration.underline; - gestureRecognizer = TapGestureRecognizer() - ..onTap = () { - // TODO: open the link - }; - } - return TextSpan( - text: textInsert.content, - style: TextStyle( - fontWeight: fontWeight, - fontStyle: fontStyle, - decoration: decoration, - color: color, - ), - recognizer: gestureRecognizer); -} - -extension on TextNode { - List toTextSpans() { - final result = []; - - for (final op in delta.operations) { - if (op is TextInsert) { - result.add(_textInsertToTextSpan(op)); - } - } - - return result; - } -} - class _TextNodeWidget extends StatefulWidget { final Node node; final EditorState editorState; @@ -99,7 +39,9 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> implements TextInputClient { TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; - TextEditingValue get textEditingValue => const TextEditingValue(); + TextEditingValue get textEditingValue => TextEditingValue( + text: node.toRawString(), + ); TextInputConnection? _textInputConnection; @@ -136,7 +78,10 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> editorState: editorState, ), ), - ) + ), + const SizedBox( + height: 10, + ), ], ); } @@ -195,3 +140,52 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> // TODO: implement updateFloatingCursor } } + +extension on TextNode { + List toTextSpans() => delta.operations + .whereType() + .map((op) => _textInsertToTextSpan(op)) + .toList(); + + String toRawString() => delta.operations + .whereType() + .map((op) => op.content) + .toString(); +} + +TextSpan _textInsertToTextSpan(TextInsert textInsert) { + FontWeight? fontWeight; + FontStyle? fontStyle; + TextDecoration? decoration; + GestureRecognizer? gestureRecognizer; + Color? color; + final attributes = textInsert.attributes; + if (attributes?['bold'] == true) { + fontWeight = FontWeight.bold; + } + if (attributes?['italic'] == true) { + fontStyle = FontStyle.italic; + } + if (attributes?['underline'] == true) { + decoration = TextDecoration.underline; + } + if (attributes?['href'] is String) { + color = const Color.fromARGB(255, 55, 120, 245); + decoration = TextDecoration.underline; + gestureRecognizer = TapGestureRecognizer() + ..onTap = () { + launchUrlString(attributes?['href']); + }; + } + return TextSpan( + text: textInsert.content, + style: TextStyle( + fontWeight: fontWeight, + fontStyle: fontStyle, + decoration: decoration, + color: color, + fontSize: 16, + ), + recognizer: gestureRecognizer, + ); +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc index e71a16d23d..f6f23bfe97 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + 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/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake index 2e1de87a7e..f16b4c3421 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b608..4b81f9b2d2 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig index c2efd0b608..5caa9d1579 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817a52..8236f5728c 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile new file mode 100644 index 0000000000..dade8dfad0 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock new file mode 100644 index 0000000000..4f162e68af --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - FlutterMacOS (1.0.0) + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + +PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c + +COCOAPODS: 1.11.3 diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj index c84862c675..057a1a8224 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 8FD791997F0D60CE136153FB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F21284F13DB2F7E10C6EB1F7 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,7 +55,7 @@ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -66,8 +67,12 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4C1351C0AA74138239028404 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + BBAF6135AB8D71FE6D8B315C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + BE3A038D8FDF07F3AD1C02FB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + F21284F13DB2F7E10C6EB1F7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,6 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8FD791997F0D60CE136153FB /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,6 +105,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 7B5E3B15415D0C17244EF9E7 /* Pods */, ); sourceTree = ""; }; @@ -145,9 +152,21 @@ path = Runner; sourceTree = ""; }; + 7B5E3B15415D0C17244EF9E7 /* Pods */ = { + isa = PBXGroup; + children = ( + BBAF6135AB8D71FE6D8B315C /* Pods-Runner.debug.xcconfig */, + 4C1351C0AA74138239028404 /* Pods-Runner.release.xcconfig */, + BE3A038D8FDF07F3AD1C02FB /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + F21284F13DB2F7E10C6EB1F7 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -159,11 +178,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 87BB3D0057F20B3618A17B82 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 09CDF3F9864A27F94DEE8EC6 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -233,6 +254,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 09CDF3F9864A27F94DEE8EC6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -270,6 +308,28 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 87BB3D0057F20B3618A17B82 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16ed..21a3cc14c7 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock index 1420c9b23d..6dd22ff45f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock @@ -81,6 +81,18 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" lints: dependency: transitive description: @@ -123,6 +135,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" provider: dependency: "direct main" description: @@ -177,6 +196,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.9" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.5" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" vector_math: dependency: transitive description: @@ -186,4 +261,4 @@ packages: version: "2.1.2" sdks: dart: ">=2.17.0 <3.0.0" - flutter: ">=1.17.0" + flutter: ">=2.10.0" diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml index d514607eac..11df9b36ee 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: flowy_editor: path: ../ provider: ^6.0.3 + url_launcher: ^6.1.5 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc index 8b6d4680af..4f7884874d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake index b93c4c30c1..88b22e5c77 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 9307edb50d79482669e3c1061d524df8c2109205 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 18 Jul 2022 17:19:59 +0800 Subject: [PATCH 030/121] feat: transaction builder --- .../example/lib/plugin/image_node_widget.dart | 11 +-- .../example/lib/plugin/text_node_widget.dart | 2 +- .../flowy_editor/lib/document/node.dart | 15 +++- .../flowy_editor/lib/document/state_tree.dart | 13 ++++ .../flowy_editor/lib/editor_state.dart | 21 +----- .../flowy_editor/lib/operation/operation.dart | 4 +- .../lib/operation/transaction.dart | 26 ++++++- .../lib/operation/transaction_builder.dart | 70 +++++++++++++++++++ 8 files changed, 132 insertions(+), 30 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index c48941011f..82d5b59ccb 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; @@ -33,10 +34,12 @@ class _ImageNodeWidget extends StatelessWidget { return GestureDetector( child: _build(context), onTap: () { - editorState.update(node, { - 'image_src': - "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400" - }); + TransactionBuilder(editorState) + ..updateNode(node, { + 'image_src': + "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400" + }) + ..commit(); }, ); } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 49dcfad8f4..5d75bd7ca4 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -2,6 +2,7 @@ import 'package:flowy_editor/document/text_delta.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flutter/services.dart'; import 'package:flowy_editor/document/attributes.dart'; @@ -187,7 +188,6 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override void updateEditingValue(TextEditingValue value) { debugPrint(value.text); - editorState.update(node, {'content': value.text}); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index ba4cb10525..4d2becf9fe 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -159,12 +159,21 @@ class Node extends ChangeNotifier with LinkedListEntry { } class TextNode extends Node { - final Delta delta; + Delta _delta; TextNode({ required super.type, required super.children, required super.attributes, - required this.delta, - }); + required Delta delta, + }) : _delta = delta; + + Delta get delta { + return _delta; + } + + set delta(Delta v) { + _delta = v; + notifyListeners(); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 1b85eb515f..b6dbd26fff 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -1,5 +1,6 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/text_delta.dart'; import './attributes.dart'; class StateTree { @@ -35,6 +36,18 @@ class StateTree { return true; } + bool textEdit(Path path, Delta delta) { + if (path.isEmpty) { + return false; + } + var node = root.childAtPath(path); + if (node == null || node is! TextNode) { + return false; + } + node.delta = node.delta.compose(delta); + return false; + } + Node? delete(Path path) { if (path.isEmpty) { return null; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index a5fbb9e40d..74c8a8e58c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -36,25 +36,6 @@ class EditorState { } } - // TODO: move to a better place. - void update(Node node, Attributes attributes) { - _applyOperation(UpdateOperation( - path: node.path, - attributes: Attributes.from(node.attributes)..addAll(attributes), - oldAttributes: node.attributes, - )); - } - - // TODO: move to a better place. - void delete(Node node) { - _applyOperation( - DeleteOperation( - path: node.path, - removedValue: node, - ), - ); - } - void _applyOperation(Operation op) { if (op is InsertOperation) { document.insert(op.path, op.value); @@ -62,6 +43,8 @@ class EditorState { document.update(op.path, op.attributes); } else if (op is DeleteOperation) { document.delete(op.path); + } else if (op is TextEditOperation) { + document.textEdit(op.path, op.delta); } } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index 5fb9a523a8..e3710ddb3c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -67,14 +67,16 @@ class DeleteOperation extends Operation { class TextEditOperation extends Operation { final Path path; final Delta delta; + final Delta inverted; TextEditOperation({ required this.path, required this.delta, + required this.inverted, }); @override Operation invert() { - return TextEditOperation(path: path, delta: delta); + return TextEditOperation(path: path, delta: inverted, inverted: delta); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart index 1a2c4bcdb5..fa56484ae3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart @@ -1,6 +1,28 @@ +import 'dart:collection'; +import 'package:flutter/material.dart'; +import 'package:flowy_editor/document/selection.dart'; import './operation.dart'; +/// This class to use to store the **changes** +/// will be applied to the editor. +/// +/// This class is immutable version the the class +/// [[Transaction]]. Is used to stored and +/// transmit. If you want to build the transaction, +/// use [[Transaction]] directly. +/// +/// There will be several ways to consume the transaction: +/// 1. Apply to the state to update the UI. +/// 2. Send to the backend to store and do operation transforming. +/// 3. Stored by the UndoManager to implement redo/undo. +/// +@immutable class Transaction { - final List operations; - Transaction([this.operations = const []]); + final UnmodifiableListView operations; + final Selection? cursorSelection; + + const Transaction({ + required this.operations, + this.cursorSelection, + }); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart new file mode 100644 index 0000000000..3736d054a4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -0,0 +1,70 @@ +import 'dart:collection'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/document/attributes.dart'; +import 'package:flowy_editor/document/selection.dart'; + +import './operation.dart'; +import './transaction.dart'; + +/// +/// This class is used to +/// build the transaction from the state. +/// +/// This class automatically save the +/// cursor from the state. +/// +/// When the transaction is undo, the +/// cursor can be restored. +/// +class TransactionBuilder { + final List operations = []; + EditorState state; + Selection? cursorSelection; + + TransactionBuilder(this.state); + + commit() { + final transaction = finish(); + state.apply(transaction); + } + + void insertNode(Path path, Node node) { + cursorSelection = state.cursorSelection; + operations.add(InsertOperation(path: path, value: node)); + } + + void updateNode(Node node, Attributes attributes) { + cursorSelection = state.cursorSelection; + operations.add(UpdateOperation( + path: node.path, + attributes: Attributes.from(node.attributes)..addAll(attributes), + oldAttributes: node.attributes, + )); + } + + void deleteNode(Node node) { + cursorSelection = state.cursorSelection; + operations.add(DeleteOperation(path: node.path, removedValue: node)); + } + + void textEdit(TextNode node, Delta Function() f) { + cursorSelection = state.cursorSelection; + final path = node.path; + + final delta = f(); + + final inverted = delta.invert(node.delta); + operations + .add(TextEditOperation(path: path, delta: delta, inverted: inverted)); + } + + Transaction finish() { + return Transaction( + operations: UnmodifiableListView(operations), + cursorSelection: cursorSelection, + ); + } +} From e9ce4c5f05585ac3ec528dc16f50b72846495f20 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 18 Jul 2022 18:39:44 +0800 Subject: [PATCH 031/121] fix: make the finish() function private --- .../flowy_editor/lib/operation/transaction_builder.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 3736d054a4..51b05f187b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -27,7 +27,7 @@ class TransactionBuilder { TransactionBuilder(this.state); commit() { - final transaction = finish(); + final transaction = _finish(); state.apply(transaction); } @@ -61,7 +61,7 @@ class TransactionBuilder { .add(TextEditOperation(path: path, delta: delta, inverted: inverted)); } - Transaction finish() { + Transaction _finish() { return Transaction( operations: UnmodifiableListView(operations), cursorSelection: cursorSelection, From b75c142433b2883d5a571a94624e5711b06b662f Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 18 Jul 2022 19:00:16 +0800 Subject: [PATCH 032/121] feat: re-export transaction_builder --- .../flowy_editor/example/lib/plugin/image_node_widget.dart | 1 - frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 82d5b59ccb..6884a7aedc 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,4 +1,3 @@ -import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index d4c0a8d70a..f816778603 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -6,5 +6,6 @@ export 'package:flowy_editor/document/path.dart'; export 'package:flowy_editor/render/render_plugins.dart'; export 'package:flowy_editor/render/node_widget_builder.dart'; export 'package:flowy_editor/operation/transaction.dart'; +export 'package:flowy_editor/operation/transaction_builder.dart'; export 'package:flowy_editor/operation/operation.dart'; export 'package:flowy_editor/editor_state.dart'; From 9c43c465f84e34e12c506466e0cced31190c611e Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 18 Jul 2022 19:38:42 +0800 Subject: [PATCH 033/121] chore: delete unused import --- .../flowy_editor/example/lib/plugin/text_node_widget.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 5d75bd7ca4..bc57241a51 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -2,7 +2,6 @@ import 'package:flowy_editor/document/text_delta.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flutter/services.dart'; import 'package:flowy_editor/document/attributes.dart'; From f910b9dc61cb757cd992c47f431f368902122f66 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 18 Jul 2022 18:54:08 +0800 Subject: [PATCH 034/121] feat: delta to string --- .../flowy_editor/assets/document.json | 5 ++ .../flowy_editor/lib/document/node.dart | 7 +++ .../flowy_editor/lib/document/state_tree.dart | 2 +- .../flowy_editor/lib/document/text_delta.dart | 63 +++++++++++++------ .../flowy_editor/test/delta_test.dart | 33 ++++++++++ 5 files changed, 91 insertions(+), 19 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json index 31092286f8..fb3628de47 100644 --- a/frontend/app_flowy/packages/flowy_editor/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json @@ -4,18 +4,21 @@ "children": [ { "type": "text", + "delta": [], "attributes": { "subtype": "with-heading" } }, { "type": "text", + "delta": [], "attributes": { "tag": "*" }, "children": [ { "type": "text", + "delta": [], "attributes": { "text-type": "heading2", "check": true @@ -23,6 +26,7 @@ }, { "type": "text", + "delta": [], "attributes": { "text-type": "checkbox", "check": true @@ -30,6 +34,7 @@ }, { "type": "text", + "delta": [], "attributes": { "tag": "**" } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 4d2becf9fe..0220827098 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -176,4 +176,11 @@ class TextNode extends Node { _delta = v; notifyListeners(); } + + @override + Map toJson() { + final map = super.toJson(); + map['delta'] = _delta.toJson(); + return map; + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index b6dbd26fff..22f4b88c24 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -40,7 +40,7 @@ class StateTree { if (path.isEmpty) { return false; } - var node = root.childAtPath(path); + final node = root.childAtPath(path); if (node == null || node is! TextNode) { return false; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index b87ca71b95..ede7d65882 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'dart:math'; +import 'package:flowy_editor/document/attributes.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import './attributes.dart'; @@ -8,7 +9,7 @@ import './attributes.dart'; // constant number: 2^53 - 1 const int _maxInt = 9007199254740991; -class TextOperation { +abstract class TextOperation { bool get isEmpty { return length == 0; } @@ -20,6 +21,8 @@ class TextOperation { Attributes? get attributes { return null; } + + Map toJson(); } class TextInsert extends TextOperation { @@ -54,6 +57,18 @@ class TextInsert extends TextOperation { return Object.hash( contentHash, attrs == null ? null : hashAttributes(attrs)); } + + @override + Map toJson() { + final result = { + 'insert': content, + }; + final attrs = _attributes; + if (attrs != null) { + result['attributes'] = {...attrs}; + } + return result; + } } class TextRetain extends TextOperation { @@ -96,6 +111,18 @@ class TextRetain extends TextOperation { final attrs = _attributes; return Object.hash(_length, attrs == null ? null : hashAttributes(attrs)); } + + @override + Map toJson() { + final result = { + 'retain': _length, + }; + final attrs = _attributes; + if (attrs != null) { + result['attributes'] = {...attrs}; + } + return result; + } } class TextDelete extends TextOperation { @@ -129,6 +156,13 @@ class TextDelete extends TextOperation { int get hashCode { return _length.hashCode; } + + @override + Map toJson() { + return { + 'delete': _length, + }; + } } class _OpIterator { @@ -215,28 +249,17 @@ class _OpIterator { } } -Attributes? _attributesFromJSON(Map? json) { - if (json == null) { - return null; - } - final result = {}; - - for (final entry in json.entries) { - result[entry.key] = entry.value; - } - - return result; -} - TextOperation? _textOperationFromJson(Map json) { TextOperation? result; if (json['insert'] is String) { - result = TextInsert(json['insert'] as String, - _attributesFromJSON(json['attributes'] as Map?)); + final attrs = json['attributes'] as Map?; + result = + TextInsert(json['insert'] as String, attrs == null ? null : {...attrs}); } else if (json['retain'] is int) { - result = TextRetain(json['retain'] as int, - _attributesFromJSON(json['attributes'] as Map?)); + final attrs = json['attributes'] as Map?; + result = + TextRetain(json['retain'] as int, attrs == null ? null : {...attrs}); } else if (json['delete'] is int) { result = TextDelete(json['delete'] as int); } @@ -459,4 +482,8 @@ class Delta { }); return inverted.chop(); } + + List toJson() { + return operations.map((e) => e.toJson()).toList(); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index bce2517744..9a914888d4 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -197,4 +197,37 @@ void main() { // expect(base.compose(delta).compose(inverted), base); // }); }); + group('json', () { + test('toJson()', () { + final delta = Delta().retain(2).insert('A').delete(3); + expect(delta.toJson(), [ + {'retain': 2}, + {'insert': 'A'}, + {'delete': 3} + ]); + }); + test('attributes', () { + final delta = + Delta().retain(2, {'bold': true}).insert('A', {'italic': true}); + expect(delta.toJson(), [ + { + 'retain': 2, + 'attributes': {'bold': true}, + }, + { + 'insert': 'A', + 'attributes': {'italic': true}, + }, + ]); + }); + test('fromJson()', () { + final delta = Delta.fromJson([ + {'retain': 2}, + {'insert': 'A'}, + {'delete': 3}, + ]); + final expected = Delta().retain(2).insert('A').delete(3); + expect(delta, expected); + }); + }); } From f526d29f594ff90d607ed239d56c5ac44b372fe3 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 18 Jul 2022 19:46:29 +0800 Subject: [PATCH 035/121] chore: support https request in macOS platform. --- .../packages/flowy_editor/example/assets/document.json | 6 ++++++ .../example/macos/Runner/DebugProfile.entitlements | 2 ++ .../flowy_editor/example/macos/Runner/Release.entitlements | 2 ++ 3 files changed, 10 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index d8012e857c..7692c8b00b 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -3,6 +3,12 @@ "type": "editor", "attributes": {}, "children": [ + { + "type": "image", + "attributes": { + "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=1500w" + } + }, { "type": "text", "delta": [ diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements index dddb8a30c8..c946719a1a 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements index 852fa1a472..48271acc95 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client + From ac228a5316ca9ef3e6dc8935acad5f6442952c3c Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 18 Jul 2022 20:13:33 +0800 Subject: [PATCH 036/121] refactor: better abstract style --- .../flowy_editor/lib/document/text_delta.dart | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index ede7d65882..2f3d194255 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -10,17 +10,11 @@ import './attributes.dart'; const int _maxInt = 9007199254740991; abstract class TextOperation { - bool get isEmpty { - return length == 0; - } + bool get isEmpty => length == 0; - int get length { - return 0; - } + int get length; - Attributes? get attributes { - return null; - } + Attributes? get attributes => null; Map toJson(); } @@ -130,11 +124,6 @@ class TextDelete extends TextOperation { TextDelete(int length) : _length = length; - @override - bool get isEmpty { - return length == 0; - } - @override int get length { return _length; From fef9e20e467cf6fc0162e82383fa3edd36bd3191 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 19 Jul 2022 14:31:10 +0800 Subject: [PATCH 037/121] chore: add heading widget to example --- .../flowy_editor/example/assets/document.json | 75 +++++++++++-------- .../flowy_editor/example/lib/main.dart | 4 +- .../example/lib/plugin/image_node_widget.dart | 12 +-- .../example/lib/plugin/text_node_widget.dart | 21 +++++- .../plugin/text_with_heading_node_widget.dart | 45 +++++++++++ 5 files changed, 114 insertions(+), 43 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 7692c8b00b..c2ba9fbb09 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -12,50 +12,65 @@ { "type": "text", "delta": [ - { "insert": "With " }, - { "insert": "AppFlowy", "attributes": { "href": "https://www.appflowy.io/" } }, - { "insert": ", you can build detailed lists of to-do’s for different projects while tracking the status of each one" } + { "insert": "👋 Welcome to AppFlowy!", "attributes": { "href": "https://www.appflowy.io/", "heading": "h1" } } + ], + "attributes": { + "subtype": "with-heading", + "heading": "h1" + } + }, + { + "type": "text", + "delta": [ + { "insert": "Here are the basics", "attributes": { "heading": "h2" } } + ], + "attributes": { + "subtype": "with-heading", + "heading": "h2" + } + }, + { + "type": "text", + "delta": [ + { "insert": "Click anywhere and just start typing." } + ], + "attributes": { + "subtype": "with-checkbox", + "checkbox": true + } + }, + { + "type": "text", + "delta": [ + { "insert": "Highlight", "attributes": { "highlight": "0xFFFFFF00" } }, + { "insert": " Click anywhere and just start typing" }, + { "insert": " any text, and use the menu at the bottom to " }, + { "insert": "style", "attributes": { "italic": true } }, + { "insert": " your ", "attributes": { "bold": true } }, + { "insert": "writing", "attributes": { "underline": true } }, + { "insert": " howeverv you like.", "attributes": { "strikethrough": true } } ], "attributes": { "subtype": "with-checkbox", - "font-size": 30, "checkbox": false } }, { "type": "text", "delta": [ - { "insert": "You can " }, - { "insert": "host", "attributes": { "italic": true } }, - { "insert": " " }, - { "insert": "AppFlowy", "attributes": { "bold": true } }, - { "insert": " " }, - { "insert": "wherever you want", "attributes": { "underline": true }}, - { "insert": "; no vendor lock-in." } + { "insert": "Have a question? ", "attributes": { "heading": "h2" } } ], - "attributes": { - "subtype": "with-checkbox", - "text-type": "heading1", - "font-size": 30, - "checkbox": false + "attributes": { + "subtype": "with-heading", + "heading": "h2" } }, { "type": "text", - "delta": [{ "insert": "Design and modify AppFlowy your way with an open core codebase." }], - "attributes": { - "text-type": "heading1", - "font-size": 30 - } - }, - { - "type": "text", - "delta": [{ "insert": "AppFlowy is built with Flutter and Rust. What does this mean? Faster development, better native experience, and more reliable performance." }], - "attributes": { - "text-type": "heading1", - "font-size": 30, - "content": "dddddddddddddddddddd" - } + "delta": [ + { "insert": "Click the '?' at the bottom right for help and support."} + ], + "attributes": {} } ] } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index cc9acce929..8a413f78b1 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:example/plugin/document_node_widget.dart'; +import 'package:example/plugin/text_with_heading_node_widget.dart'; import 'package:example/plugin/image_node_widget.dart'; import 'package:example/plugin/text_node_widget.dart'; import 'package:example/plugin/text_with_check_box_node_widget.dart'; @@ -66,7 +67,8 @@ class _MyHomePageState extends State { ..register('editor', EditorNodeWidgetBuilder.create) ..register('text', TextNodeBuilder.create) ..register('image', ImageNodeBuilder.create) - ..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create); + ..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create) + ..register('text/with-heading', TextWithHeadingNodeBuilder.create); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 6884a7aedc..692d00baf2 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -30,17 +30,7 @@ class _ImageNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - child: _build(context), - onTap: () { - TransactionBuilder(editorState) - ..updateNode(node, { - 'image_src': - "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400" - }) - ..commit(); - }, - ); + return _build(context); } Widget _build(BuildContext context) { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index d58f2fef1d..2b66e3ea7f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -158,6 +158,8 @@ TextSpan _textInsertToTextSpan(TextInsert textInsert) { TextDecoration? decoration; GestureRecognizer? gestureRecognizer; Color? color; + Color highLightColor = Colors.transparent; + double fontSize = 16.0; final attributes = textInsert.attributes; if (attributes?['bold'] == true) { fontWeight = FontWeight.bold; @@ -168,6 +170,12 @@ TextSpan _textInsertToTextSpan(TextInsert textInsert) { if (attributes?['underline'] == true) { decoration = TextDecoration.underline; } + if (attributes?['strikethrough'] == true) { + decoration = TextDecoration.lineThrough; + } + if (attributes?['highlight'] is String) { + highLightColor = Color(int.parse(attributes!['highlight'])); + } if (attributes?['href'] is String) { color = const Color.fromARGB(255, 55, 120, 245); decoration = TextDecoration.underline; @@ -176,6 +184,16 @@ TextSpan _textInsertToTextSpan(TextInsert textInsert) { launchUrlString(attributes?['href']); }; } + final heading = attributes?['heading'] as String?; + if (heading != null) { + // TODO: make it better + if (heading == 'h1') { + fontSize = 30.0; + } else if (heading == 'h2') { + fontSize = 20.0; + } + fontWeight = FontWeight.bold; + } return TextSpan( text: textInsert.content, style: TextStyle( @@ -183,7 +201,8 @@ TextSpan _textInsertToTextSpan(TextInsert textInsert) { fontStyle: fontStyle, decoration: decoration, color: color, - fontSize: 16, + fontSize: fontSize, + backgroundColor: highLightColor, ), recognizer: gestureRecognizer, ); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart new file mode 100644 index 0000000000..9519e130f2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart @@ -0,0 +1,45 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; + +class TextWithHeadingNodeBuilder extends NodeWidgetBuilder { + TextWithHeadingNodeBuilder.create({ + required super.editorState, + required super.node, + }) : super.create() { + nodeValidator = (node) => node.attributes.containsKey('heading'); + } + + String get heading => node.attributes['heading'] as String; + Widget buildPadding() { + if (heading == 'h1') { + return const Padding( + padding: EdgeInsets.only(top: 10), + ); + } else if (heading == 'h1') { + return const Padding( + padding: EdgeInsets.only(top: 10), + ); + } + return const Padding( + padding: EdgeInsets.only(top: 0), + ); + } + + @override + Widget build(BuildContext buildContext) { + return Column( + children: [ + buildPadding(), + renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: buildContext, + node: node, + editorState: editorState, + ), + withSubtype: false, + ), + buildPadding(), + ], + ); + } +} From abe0658cd3a8a3e8679207d49ff9efbaad454f5e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 19 Jul 2022 15:24:51 +0800 Subject: [PATCH 038/121] feat: insert text at cursor --- .../example/lib/plugin/text_node_widget.dart | 45 ++++++++++++++--- .../lib/operation/transaction_builder.dart | 49 +++++++++++++++---- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index bc57241a51..517c0cda4b 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -95,11 +95,20 @@ class _TextNodeWidget extends StatefulWidget { State<_TextNodeWidget> createState() => __TextNodeWidgetState(); } +String _textContentOfDelta(Delta delta) { + return delta.operations.fold("", (previousValue, element) { + if (element is TextInsert) { + return previousValue + element.content; + } + return previousValue; + }); +} + class __TextNodeWidgetState extends State<_TextNodeWidget> - implements TextInputClient { + implements DeltaTextInputClient { TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; - TextEditingValue get textEditingValue => const TextEditingValue(); + TextSelection? _localSelection; TextInputConnection? _textInputConnection; @@ -112,20 +121,22 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> TextSpan( children: node.toTextSpans(), ), - onTap: () { + onSelectionChanged: ((selection, cause) { _textInputConnection?.close(); _textInputConnection = TextInput.attach( this, const TextInputConfiguration( - enableDeltaModel: false, + enableDeltaModel: true, inputType: TextInputType.multiline, textCapitalization: TextCapitalization.sentences, ), ); + debugPrint('selection: $selection'); _textInputConnection ?..show() - ..setEditingState(textEditingValue); - }, + ..setEditingState(TextEditingValue( + text: _textContentOfDelta(node.delta), selection: selection)); + }), ), if (node.children.isNotEmpty) ...node.children.map( @@ -152,7 +163,9 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override // TODO: implement currentTextEditingValue - TextEditingValue? get currentTextEditingValue => textEditingValue; + TextEditingValue? get currentTextEditingValue => TextEditingValue( + text: _textContentOfDelta(node.delta), + selection: _localSelection ?? const TextSelection.collapsed(offset: -1)); @override void insertTextPlaceholder(Size size) { @@ -186,7 +199,23 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override void updateEditingValue(TextEditingValue value) { - debugPrint(value.text); + debugPrint('offset: ${value.selection}'); + } + + @override + void updateEditingValueWithDeltas(List textEditingDeltas) { + for (final textDelta in textEditingDeltas) { + if (textDelta is TextEditingDeltaInsertion) { + TransactionBuilder(editorState) + ..insertText(node, textDelta.insertionOffset, textDelta.textInserted) + ..commit(); + } else if (textDelta is TextEditingDeltaDeletion) { + TransactionBuilder(editorState) + ..deleteText(node, textDelta.deletedRange.start, + textDelta.deletedRange.end - textDelta.deletedRange.start) + ..commit(); + } + } } @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 51b05f187b..9635c0ebc5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -26,39 +26,70 @@ class TransactionBuilder { TransactionBuilder(this.state); + /// Commit the operations to the state commit() { final transaction = _finish(); state.apply(transaction); } - void insertNode(Path path, Node node) { + insertNode(Path path, Node node) { cursorSelection = state.cursorSelection; - operations.add(InsertOperation(path: path, value: node)); + add(InsertOperation(path: path, value: node)); } - void updateNode(Node node, Attributes attributes) { + updateNode(Node node, Attributes attributes) { cursorSelection = state.cursorSelection; - operations.add(UpdateOperation( + add(UpdateOperation( path: node.path, attributes: Attributes.from(node.attributes)..addAll(attributes), oldAttributes: node.attributes, )); } - void deleteNode(Node node) { + deleteNode(Node node) { cursorSelection = state.cursorSelection; - operations.add(DeleteOperation(path: node.path, removedValue: node)); + add(DeleteOperation(path: node.path, removedValue: node)); } - void textEdit(TextNode node, Delta Function() f) { + textEdit(TextNode node, Delta Function() f) { cursorSelection = state.cursorSelection; final path = node.path; final delta = f(); final inverted = delta.invert(node.delta); - operations - .add(TextEditOperation(path: path, delta: delta, inverted: inverted)); + + add(TextEditOperation(path: path, delta: delta, inverted: inverted)); + } + + insertText(TextNode node, int index, String content) { + textEdit(node, () => Delta().retain(index).insert(content)); + } + + formatText(TextNode node, int index, int length, Attributes attributes) { + textEdit(node, () => Delta().retain(index).retain(length, attributes)); + } + + deleteText(TextNode node, int index, int length) { + textEdit(node, () => Delta().retain(index).delete(length)); + } + + add(Operation op) { + final Operation? last = operations.isEmpty ? null : operations.last; + if (last != null) { + if (op is TextEditOperation && + last is TextEditOperation && + pathEquals(op.path, last.path)) { + final newOp = TextEditOperation( + path: op.path, + delta: last.delta.compose(op.delta), + inverted: op.inverted.compose(last.inverted), + ); + operations[operations.length - 1] = newOp; + return; + } + } + operations.add(op); } Transaction _finish() { From 8c6c9f7c0de275e6bb159baadb857534c9771304 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 19 Jul 2022 16:56:53 +0800 Subject: [PATCH 039/121] feat: transform betweens global/local cursor --- .../example/lib/plugin/text_node_widget.dart | 125 +++++++++++++++--- .../flowy_editor/lib/editor_state.dart | 1 + .../lib/operation/transaction.dart | 6 +- .../lib/operation/transaction_builder.dart | 20 ++- 4 files changed, 122 insertions(+), 30 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 517c0cda4b..edacd6716e 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -1,3 +1,5 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -81,6 +83,40 @@ extension on TextNode { } } +TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) { + if (globalSel == null) { + return null; + } + final nodePath = node.path; + + if (!pathEquals(nodePath, globalSel.start.path)) { + return null; + } + if (globalSel.isCollapsed()) { + return TextSelection( + baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset); + } else { + if (pathEquals(globalSel.start.path, globalSel.end.path)) { + return TextSelection( + baseOffset: globalSel.start.offset, + extentOffset: globalSel.end.offset); + } + } + return null; +} + +Selection? _localSelectionToGlobal(Node node, TextSelection? sel) { + if (sel == null) { + return null; + } + final nodePath = node.path; + + return Selection( + start: Position(path: nodePath, offset: sel.baseOffset), + end: Position(path: nodePath, offset: sel.extentOffset), + ); +} + class _TextNodeWidget extends StatefulWidget { final Node node; final EditorState editorState; @@ -106,37 +142,80 @@ String _textContentOfDelta(Delta delta) { class __TextNodeWidgetState extends State<_TextNodeWidget> implements DeltaTextInputClient { + final _focusNode = FocusNode(debugLabel: "input"); TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; - TextSelection? _localSelection; TextInputConnection? _textInputConnection; + _backDeleteTextAtSelection(TextSelection? sel) { + if (sel == null) { + return; + } + if (sel.start == 0) { + return; + } + + if (sel.isCollapsed) { + TransactionBuilder(editorState) + ..deleteText(node, sel.start - 1, 1) + ..commit(); + } else { + TransactionBuilder(editorState) + ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset) + ..commit(); + } + + _textInputConnection?.setEditingState(TextEditingValue( + text: _textContentOfDelta(node.delta), + selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? + const TextSelection.collapsed(offset: 0))); + } + @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SelectableText.rich( - TextSpan( - children: node.toTextSpans(), - ), - onSelectionChanged: ((selection, cause) { - _textInputConnection?.close(); - _textInputConnection = TextInput.attach( - this, - const TextInputConfiguration( - enableDeltaModel: true, - inputType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - ), - ); - debugPrint('selection: $selection'); - _textInputConnection - ?..show() - ..setEditingState(TextEditingValue( - text: _textContentOfDelta(node.delta), selection: selection)); + KeyboardListener( + focusNode: _focusNode, + onKeyEvent: ((value) { + if (value is KeyDownEvent || value is KeyRepeatEvent) { + final sel = + _globalSelectionToLocal(node, editorState.cursorSelection); + if (value.logicalKey.keyLabel == "Backspace") { + _backDeleteTextAtSelection(sel); + } + } }), + child: SelectableText.rich( + showCursor: true, + TextSpan( + children: node.toTextSpans(), + ), + onTap: () { + _focusNode.requestFocus(); + }, + onSelectionChanged: ((selection, cause) { + _textInputConnection?.close(); + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + enableDeltaModel: true, + inputType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + ), + ); + debugPrint('selection: $selection'); + editorState.cursorSelection = + _localSelectionToGlobal(node, selection); + _textInputConnection + ?..show() + ..setEditingState(TextEditingValue( + text: _textContentOfDelta(node.delta), + selection: selection)); + }), + ), ), if (node.children.isNotEmpty) ...node.children.map( @@ -165,7 +244,8 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> // TODO: implement currentTextEditingValue TextEditingValue? get currentTextEditingValue => TextEditingValue( text: _textContentOfDelta(node.delta), - selection: _localSelection ?? const TextSelection.collapsed(offset: -1)); + selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? + const TextSelection.collapsed(offset: 0)); @override void insertTextPlaceholder(Size size) { @@ -174,7 +254,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override void performAction(TextInputAction action) { - // TODO: implement performAction + debugPrint('action:$action'); } @override @@ -204,6 +284,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override void updateEditingValueWithDeltas(List textEditingDeltas) { + debugPrint(textEditingDeltas.toString()); for (final textDelta in textEditingDeltas) { if (textDelta is TextEditingDeltaInsertion) { TransactionBuilder(editorState) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 74c8a8e58c..521231a495 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -34,6 +34,7 @@ class EditorState { for (final op in transaction.operations) { _applyOperation(op); } + cursorSelection = transaction.afterSelection; } void _applyOperation(Operation op) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart index fa56484ae3..3de528e868 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart @@ -19,10 +19,12 @@ import './operation.dart'; @immutable class Transaction { final UnmodifiableListView operations; - final Selection? cursorSelection; + final Selection? beforeSelection; + final Selection? afterSelection; const Transaction({ required this.operations, - this.cursorSelection, + this.beforeSelection, + this.afterSelection, }); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 9635c0ebc5..ec088dc25d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -1,7 +1,9 @@ import 'dart:collection'; +import 'dart:math'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/selection.dart'; @@ -22,7 +24,8 @@ import './transaction.dart'; class TransactionBuilder { final List operations = []; EditorState state; - Selection? cursorSelection; + Selection? beforeSelection; + Selection? afterSelection; TransactionBuilder(this.state); @@ -33,12 +36,12 @@ class TransactionBuilder { } insertNode(Path path, Node node) { - cursorSelection = state.cursorSelection; + beforeSelection = state.cursorSelection; add(InsertOperation(path: path, value: node)); } updateNode(Node node, Attributes attributes) { - cursorSelection = state.cursorSelection; + beforeSelection = state.cursorSelection; add(UpdateOperation( path: node.path, attributes: Attributes.from(node.attributes)..addAll(attributes), @@ -47,12 +50,12 @@ class TransactionBuilder { } deleteNode(Node node) { - cursorSelection = state.cursorSelection; + beforeSelection = state.cursorSelection; add(DeleteOperation(path: node.path, removedValue: node)); } textEdit(TextNode node, Delta Function() f) { - cursorSelection = state.cursorSelection; + beforeSelection = state.cursorSelection; final path = node.path; final delta = f(); @@ -64,6 +67,8 @@ class TransactionBuilder { insertText(TextNode node, int index, String content) { textEdit(node, () => Delta().retain(index).insert(content)); + afterSelection = Selection.collapsed( + Position(path: node.path, offset: index + content.length)); } formatText(TextNode node, int index, int length, Attributes attributes) { @@ -72,6 +77,8 @@ class TransactionBuilder { deleteText(TextNode node, int index, int length) { textEdit(node, () => Delta().retain(index).delete(length)); + afterSelection = + Selection.collapsed(Position(path: node.path, offset: index)); } add(Operation op) { @@ -95,7 +102,8 @@ class TransactionBuilder { Transaction _finish() { return Transaction( operations: UnmodifiableListView(operations), - cursorSelection: cursorSelection, + beforeSelection: beforeSelection, + afterSelection: afterSelection, ); } } From 7b513a71a952eb8aacba28695034a5aadfbd7026 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 19 Jul 2022 18:40:01 +0800 Subject: [PATCH 040/121] feat: handle Delete key --- .../example/lib/plugin/text_node_widget.dart | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index edacd6716e..81c0df2f9c 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -166,6 +166,27 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> ..commit(); } + _setEditingStateFromGlobal(); + } + + _forwardDeleteTextAtSelection(TextSelection? sel) { + if (sel == null) { + return; + } + + if (sel.isCollapsed) { + TransactionBuilder(editorState) + ..deleteText(node, sel.start, 1) + ..commit(); + } else { + TransactionBuilder(editorState) + ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset) + ..commit(); + } + _setEditingStateFromGlobal(); + } + + _setEditingStateFromGlobal() { _textInputConnection?.setEditingState(TextEditingValue( text: _textContentOfDelta(node.delta), selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? @@ -185,6 +206,8 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> _globalSelectionToLocal(node, editorState.cursorSelection); if (value.logicalKey.keyLabel == "Backspace") { _backDeleteTextAtSelection(sel); + } else if (value.logicalKey.keyLabel == "Delete") { + _forwardDeleteTextAtSelection(sel); } } }), From 30483e9d1ea16024b930fa17e17bd8a1c37a82ba Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 19 Jul 2022 21:41:48 +0800 Subject: [PATCH 041/121] chore: fix text_node_widget.dart build error --- .../example/lib/plugin/text_node_widget.dart | 254 +++++++----------- .../flowy_editor/lib/document/node.dart | 7 + 2 files changed, 103 insertions(+), 158 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index dd504305ca..df2039fbd0 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -23,98 +23,6 @@ class TextNodeBuilder extends NodeWidgetBuilder { } } -extension on Attributes { - TextStyle toTextStyle() { - return TextStyle( - color: this['color'] != null ? Colors.red : Colors.black, - fontSize: this['font-size'] != null ? 30 : 15, - ); - } -} - -TextSpan _textInsertToTextSpan(TextInsert textInsert) { - FontWeight? fontWeight; - FontStyle? fontStyle; - TextDecoration? decoration; - GestureRecognizer? gestureRecognizer; - Color? color; - final attributes = textInsert.attributes; - if (attributes?['bold'] == true) { - fontWeight = FontWeight.bold; - } - if (attributes?['italic'] == true) { - fontStyle = FontStyle.italic; - } - if (attributes?["underline"] == true) { - decoration = TextDecoration.underline; - } - if (attributes?["href"] is String) { - color = const Color.fromARGB(255, 55, 120, 245); - decoration = TextDecoration.underline; - gestureRecognizer = TapGestureRecognizer() - ..onTap = () { - // TODO: open the link - }; - } - return TextSpan( - text: textInsert.content, - style: TextStyle( - fontWeight: fontWeight, - fontStyle: fontStyle, - decoration: decoration, - color: color, - ), - recognizer: gestureRecognizer); -} - -extension on TextNode { - List toTextSpans() { - final result = []; - - for (final op in delta.operations) { - if (op is TextInsert) { - result.add(_textInsertToTextSpan(op)); - } - } - - return result; - } -} - -TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) { - if (globalSel == null) { - return null; - } - final nodePath = node.path; - - if (!pathEquals(nodePath, globalSel.start.path)) { - return null; - } - if (globalSel.isCollapsed()) { - return TextSelection( - baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset); - } else { - if (pathEquals(globalSel.start.path, globalSel.end.path)) { - return TextSelection( - baseOffset: globalSel.start.offset, - extentOffset: globalSel.end.offset); - } - } - return null; -} - -Selection? _localSelectionToGlobal(Node node, TextSelection? sel) { - if (sel == null) { - return null; - } - final nodePath = node.path; - - return Selection( - start: Position(path: nodePath, offset: sel.baseOffset), - end: Position(path: nodePath, offset: sel.extentOffset), - ); -} - class _TextNodeWidget extends StatefulWidget { final Node node; final EditorState editorState; @@ -144,10 +52,6 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; - TextEditingValue get textEditingValue => TextEditingValue( - text: node.toRawString(), - ); - TextInputConnection? _textInputConnection; _backDeleteTextAtSelection(TextSelection? sel) { @@ -190,7 +94,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> _setEditingStateFromGlobal() { _textInputConnection?.setEditingState(TextEditingValue( - text: _textContentOfDelta(node.delta), + text: node.toString(), selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? const TextSelection.collapsed(offset: 0))); } @@ -236,9 +140,12 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> _localSelectionToGlobal(node, selection); _textInputConnection ?..show() - ..setEditingState(TextEditingValue( - text: _textContentOfDelta(node.delta), - selection: selection)); + ..setEditingState( + TextEditingValue( + text: node.toString(), + selection: selection, + ), + ); }), ), ), @@ -271,7 +178,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override // TODO: implement currentTextEditingValue TextEditingValue? get currentTextEditingValue => TextEditingValue( - text: _textContentOfDelta(node.delta), + text: node.toString(), selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? const TextSelection.collapsed(offset: 0)); @@ -336,67 +243,98 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> extension on TextNode { List toTextSpans() => delta.operations .whereType() - .map((op) => _textInsertToTextSpan(op)) + .map((op) => op.toTextSpan()) .toList(); - - String toRawString() => delta.operations - .whereType() - .map((op) => op.content) - .toString(); } -TextSpan _textInsertToTextSpan(TextInsert textInsert) { - FontWeight? fontWeight; - FontStyle? fontStyle; - TextDecoration? decoration; - GestureRecognizer? gestureRecognizer; - Color? color; - Color highLightColor = Colors.transparent; - double fontSize = 16.0; - final attributes = textInsert.attributes; - if (attributes?['bold'] == true) { - fontWeight = FontWeight.bold; - } - if (attributes?['italic'] == true) { - fontStyle = FontStyle.italic; - } - if (attributes?['underline'] == true) { - decoration = TextDecoration.underline; - } - if (attributes?['strikethrough'] == true) { - decoration = TextDecoration.lineThrough; - } - if (attributes?['highlight'] is String) { - highLightColor = Color(int.parse(attributes!['highlight'])); - } - if (attributes?['href'] is String) { - color = const Color.fromARGB(255, 55, 120, 245); - decoration = TextDecoration.underline; - gestureRecognizer = TapGestureRecognizer() - ..onTap = () { - launchUrlString(attributes?['href']); - }; - } - final heading = attributes?['heading'] as String?; - if (heading != null) { - // TODO: make it better - if (heading == 'h1') { - fontSize = 30.0; - } else if (heading == 'h2') { - fontSize = 20.0; +extension on TextInsert { + TextSpan toTextSpan() { + FontWeight? fontWeight; + FontStyle? fontStyle; + TextDecoration? decoration; + GestureRecognizer? gestureRecognizer; + Color? color; + Color highLightColor = Colors.transparent; + double fontSize = 16.0; + final attributes = this.attributes; + if (attributes?['bold'] == true) { + fontWeight = FontWeight.bold; } - fontWeight = FontWeight.bold; + if (attributes?['italic'] == true) { + fontStyle = FontStyle.italic; + } + if (attributes?['underline'] == true) { + decoration = TextDecoration.underline; + } + if (attributes?['strikethrough'] == true) { + decoration = TextDecoration.lineThrough; + } + if (attributes?['highlight'] is String) { + highLightColor = Color(int.parse(attributes!['highlight'])); + } + if (attributes?['href'] is String) { + color = const Color.fromARGB(255, 55, 120, 245); + decoration = TextDecoration.underline; + gestureRecognizer = TapGestureRecognizer() + ..onTap = () { + launchUrlString(attributes?['href']); + }; + } + final heading = attributes?['heading'] as String?; + if (heading != null) { + // TODO: make it better + if (heading == 'h1') { + fontSize = 30.0; + } else if (heading == 'h2') { + fontSize = 20.0; + } + fontWeight = FontWeight.bold; + } + return TextSpan( + text: content, + style: TextStyle( + fontWeight: fontWeight, + fontStyle: fontStyle, + decoration: decoration, + color: color, + fontSize: fontSize, + backgroundColor: highLightColor, + ), + recognizer: gestureRecognizer, + ); } - return TextSpan( - text: textInsert.content, - style: TextStyle( - fontWeight: fontWeight, - fontStyle: fontStyle, - decoration: decoration, - color: color, - fontSize: fontSize, - backgroundColor: highLightColor, - ), - recognizer: gestureRecognizer, +} + +TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) { + if (globalSel == null) { + return null; + } + final nodePath = node.path; + + if (!pathEquals(nodePath, globalSel.start.path)) { + return null; + } + if (globalSel.isCollapsed()) { + return TextSelection( + baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset); + } else { + if (pathEquals(globalSel.start.path, globalSel.end.path)) { + return TextSelection( + baseOffset: globalSel.start.offset, + extentOffset: globalSel.end.offset); + } + } + return null; +} + +Selection? _localSelectionToGlobal(Node node, TextSelection? sel) { + if (sel == null) { + return null; + } + final nodePath = node.path; + + return Selection( + start: Position(path: nodePath, offset: sel.baseOffset), + end: Position(path: nodePath, offset: sel.extentOffset), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 0220827098..9d8b9906b0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -183,4 +183,11 @@ class TextNode extends Node { map['delta'] = _delta.toJson(); return map; } + + String toString() { + _delta.operations + .whereType() + .map((op) => op.content) + .toString(); + } } From ce953d802acb0d800ff91dd08161d8c616974c74 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 20 Jul 2022 10:54:35 +0800 Subject: [PATCH 042/121] feat: duplicate selectable_text.dart from flutter and modify selection implement --- .../lib/plugin/flowy_selectable_text.dart | 758 ++++++++++++++++++ .../example/lib/plugin/text_node_widget.dart | 160 ++-- .../flowy_editor/lib/document/node.dart | 10 +- 3 files changed, 839 insertions(+), 89 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart new file mode 100644 index 0000000000..0454f1cdc1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart @@ -0,0 +1,758 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; + +import 'package:flutter/material.dart'; + +/// An eyeballed value that moves the cursor slightly left of where it is +/// rendered for text on Android so its positioning more accurately matches the +/// native iOS text cursor positioning. +/// +/// This value is in device pixels, not logical pixels as is typically used +/// throughout the codebase. +const int iOSHorizontalOffset = -2; + +class _TextSpanEditingController extends TextEditingController { + _TextSpanEditingController({required TextSpan textSpan}) + : assert(textSpan != null), + _textSpan = textSpan, + super(text: textSpan.toPlainText(includeSemanticsLabels: false)); + + final TextSpan _textSpan; + + @override + TextSpan buildTextSpan( + {required BuildContext context, + TextStyle? style, + required bool withComposing}) { + // This does not care about composing. + return TextSpan( + style: style, + children: [_textSpan], + ); + } + + @override + set text(String? newText) { + // This should never be reached. + throw UnimplementedError(); + } +} + +class _SelectableTextSelectionGestureDetectorBuilder + extends TextSelectionGestureDetectorBuilder { + _SelectableTextSelectionGestureDetectorBuilder({ + required _FlowySelectableTextState state, + }) : _state = state, + super(delegate: state); + + final _FlowySelectableTextState _state; + + @override + void onForcePressStart(ForcePressDetails details) { + super.onForcePressStart(details); + if (delegate.selectionEnabled && shouldShowSelectionToolbar) { + editableText.showToolbar(); + } + } + + @override + void onForcePressEnd(ForcePressDetails details) { + // Not required. + } + + @override + void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { + if (delegate.selectionEnabled) { + renderEditable.selectWordsInRange( + from: details.globalPosition - details.offsetFromOrigin, + to: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + } + } + + @override + void onSingleTapUp(TapUpDetails details) { + editableText.hideToolbar(); + if (delegate.selectionEnabled) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + // renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); + // break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + renderEditable.selectPosition(cause: SelectionChangedCause.tap); + break; + } + } + _state.widget.onTap?.call(); + } + + @override + void onSingleLongTapStart(LongPressStartDetails details) { + if (delegate.selectionEnabled) { + renderEditable.selectWord(cause: SelectionChangedCause.longPress); + Feedback.forLongPress(_state.context); + } + } +} + +/// A run of selectable text with a single style. +/// +/// The [FlowySelectableText] widget displays a string of text with a single style. +/// The string might break across multiple lines or might all be displayed on +/// the same line depending on the layout constraints. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc} +/// +/// The [style] argument is optional. When omitted, the text will use the style +/// from the closest enclosing [DefaultTextStyle]. If the given style's +/// [TextStyle.inherit] property is true (the default), the given style will +/// be merged with the closest enclosing [DefaultTextStyle]. This merging +/// behavior is useful, for example, to make the text bold while using the +/// default font family and size. +/// +/// {@macro flutter.material.textfield.wantKeepAlive} +/// +/// {@tool snippet} +/// +/// ```dart +/// const SelectableText( +/// 'Hello! How are you?', +/// textAlign: TextAlign.center, +/// style: TextStyle(fontWeight: FontWeight.bold), +/// ) +/// ``` +/// {@end-tool} +/// +/// Using the [SelectableText.rich] constructor, the [FlowySelectableText] widget can +/// display a paragraph with differently styled [TextSpan]s. The sample +/// that follows displays "Hello beautiful world" with different styles +/// for each word. +/// +/// {@tool snippet} +/// +/// ```dart +/// const SelectableText.rich( +/// TextSpan( +/// text: 'Hello', // default text style +/// children: [ +/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)), +/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)), +/// ], +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Interactivity +/// +/// To make [FlowySelectableText] react to touch events, use callback [onTap] to achieve +/// the desired behavior. +/// +/// See also: +/// +/// * [Text], which is the non selectable version of this widget. +/// * [TextField], which is the editable version of this widget. +class FlowySelectableText extends StatefulWidget { + /// Creates a selectable text widget. + /// + /// If the [style] argument is null, the text will use the style from the + /// closest enclosing [DefaultTextStyle]. + /// + + /// The [showCursor], [autofocus], [dragStartBehavior], [selectionHeightStyle], + /// [selectionWidthStyle] and [data] parameters must not be null. If specified, + /// the [maxLines] argument must be greater than zero. + const FlowySelectableText( + String this.data, { + Key? key, + this.focusNode, + this.style, + this.strutStyle, + this.textAlign, + this.textDirection, + this.textScaleFactor, + this.showCursor = false, + this.autofocus = false, + ToolbarOptions? toolbarOptions, + this.minLines, + this.maxLines, + this.cursorWidth = 2.0, + this.cursorHeight, + this.cursorRadius, + this.cursorColor, + this.selectionHeightStyle = ui.BoxHeightStyle.tight, + this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.dragStartBehavior = DragStartBehavior.start, + this.enableInteractiveSelection = true, + this.selectionControls, + this.onTap, + this.scrollPhysics, + this.semanticsLabel, + this.textHeightBehavior, + this.textWidthBasis, + this.onSelectionChanged, + }) : assert(showCursor != null), + assert(autofocus != null), + assert(dragStartBehavior != null), + assert(selectionHeightStyle != null), + assert(selectionWidthStyle != null), + assert(maxLines == null || maxLines > 0), + assert(minLines == null || minLines > 0), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + "minLines can't be greater than maxLines", + ), + assert( + data != null, + 'A non-null String must be provided to a SelectableText widget.', + ), + textSpan = null, + toolbarOptions = toolbarOptions ?? + const ToolbarOptions( + selectAll: true, + copy: true, + ), + super(key: key); + + /// Creates a selectable text widget with a [TextSpan]. + /// + /// The [textSpan] parameter must not be null and only contain [TextSpan] in + /// [textSpan].children. Other type of [InlineSpan] is not allowed. + /// + /// The [autofocus] and [dragStartBehavior] arguments must not be null. + const FlowySelectableText.rich( + TextSpan this.textSpan, { + Key? key, + this.focusNode, + this.style, + this.strutStyle, + this.textAlign, + this.textDirection, + this.textScaleFactor, + this.showCursor = false, + this.autofocus = false, + ToolbarOptions? toolbarOptions, + this.minLines, + this.maxLines, + this.cursorWidth = 2.0, + this.cursorHeight, + this.cursorRadius, + this.cursorColor, + this.selectionHeightStyle = ui.BoxHeightStyle.tight, + this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.dragStartBehavior = DragStartBehavior.start, + this.enableInteractiveSelection = true, + this.selectionControls, + this.onTap, + this.scrollPhysics, + this.semanticsLabel, + this.textHeightBehavior, + this.textWidthBasis, + this.onSelectionChanged, + }) : assert(showCursor != null), + assert(autofocus != null), + assert(dragStartBehavior != null), + assert(maxLines == null || maxLines > 0), + assert(minLines == null || minLines > 0), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + "minLines can't be greater than maxLines", + ), + assert( + textSpan != null, + 'A non-null TextSpan must be provided to a SelectableText.rich widget.', + ), + data = null, + toolbarOptions = toolbarOptions ?? + const ToolbarOptions( + selectAll: true, + copy: true, + ), + super(key: key); + + /// The text to display. + /// + /// This will be null if a [textSpan] is provided instead. + final String? data; + + /// The text to display as a [TextSpan]. + /// + /// This will be null if [data] is provided instead. + final TextSpan? textSpan; + + /// Defines the focus for this widget. + /// + /// Text is only selectable when widget is focused. + /// + /// The [focusNode] is a long-lived object that's typically managed by a + /// [StatefulWidget] parent. See [FocusNode] for more information. + /// + /// To give the focus to this widget, provide a [focusNode] and then + /// use the current [FocusScope] to request the focus: + /// + /// ```dart + /// FocusScope.of(context).requestFocus(myFocusNode); + /// ``` + /// + /// This happens automatically when the widget is tapped. + /// + /// To be notified when the widget gains or loses the focus, add a listener + /// to the [focusNode]: + /// + /// ```dart + /// focusNode.addListener(() { print(myFocusNode.hasFocus); }); + /// ``` + /// + /// If null, this widget will create its own [FocusNode] with + /// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget + /// to be skipped over during focus traversal. + final FocusNode? focusNode; + + /// The style to use for the text. + /// + /// If null, defaults [DefaultTextStyle] of context. + final TextStyle? style; + + /// {@macro flutter.widgets.editableText.strutStyle} + final StrutStyle? strutStyle; + + /// {@macro flutter.widgets.editableText.textAlign} + final TextAlign? textAlign; + + /// {@macro flutter.widgets.editableText.textDirection} + final TextDirection? textDirection; + + /// {@macro flutter.widgets.editableText.textScaleFactor} + final double? textScaleFactor; + + /// {@macro flutter.widgets.editableText.autofocus} + final bool autofocus; + + /// {@macro flutter.widgets.editableText.minLines} + final int? minLines; + + /// {@macro flutter.widgets.editableText.maxLines} + final int? maxLines; + + /// {@macro flutter.widgets.editableText.showCursor} + final bool showCursor; + + /// {@macro flutter.widgets.editableText.cursorWidth} + final double cursorWidth; + + /// {@macro flutter.widgets.editableText.cursorHeight} + final double? cursorHeight; + + /// {@macro flutter.widgets.editableText.cursorRadius} + final Radius? cursorRadius; + + /// The color to use when painting the cursor. + /// + /// Defaults to the theme's `cursorColor` when null. + final Color? cursorColor; + + /// Controls how tall the selection highlight boxes are computed to be. + /// + /// See [ui.BoxHeightStyle] for details on available styles. + final ui.BoxHeightStyle selectionHeightStyle; + + /// Controls how wide the selection highlight boxes are computed to be. + /// + /// See [ui.BoxWidthStyle] for details on available styles. + final ui.BoxWidthStyle selectionWidthStyle; + + /// {@macro flutter.widgets.editableText.enableInteractiveSelection} + final bool enableInteractiveSelection; + + /// {@macro flutter.widgets.editableText.selectionControls} + final TextSelectionControls? selectionControls; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// Configuration of toolbar options. + /// + /// Paste and cut will be disabled regardless. + /// + /// If not set, select all and copy will be enabled by default. + final ToolbarOptions toolbarOptions; + + /// {@macro flutter.widgets.editableText.selectionEnabled} + bool get selectionEnabled => enableInteractiveSelection; + + /// Called when the user taps on this selectable text. + /// + /// The selectable text builds a [GestureDetector] to handle input events like tap, + /// to trigger focus requests, to move the caret, adjust the selection, etc. + /// Handling some of those events by wrapping the selectable text with a competing + /// GestureDetector is problematic. + /// + /// To unconditionally handle taps, without interfering with the selectable text's + /// internal gesture detector, provide this callback. + /// + /// To be notified when the text field gains or loses the focus, provide a + /// [focusNode] and add a listener to that. + /// + /// To listen to arbitrary pointer events without competing with the + /// selectable text's internal gesture detector, use a [Listener]. + final GestureTapCallback? onTap; + + /// {@macro flutter.widgets.editableText.scrollPhysics} + final ScrollPhysics? scrollPhysics; + + /// {@macro flutter.widgets.Text.semanticsLabel} + final String? semanticsLabel; + + /// {@macro dart.ui.textHeightBehavior} + final TextHeightBehavior? textHeightBehavior; + + /// {@macro flutter.painting.textPainter.textWidthBasis} + final TextWidthBasis? textWidthBasis; + + /// {@macro flutter.widgets.editableText.onSelectionChanged} + final SelectionChangedCallback? onSelectionChanged; + + @override + State createState() => _FlowySelectableTextState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + .add(DiagnosticsProperty('data', data, defaultValue: null)); + properties.add(DiagnosticsProperty('semanticsLabel', semanticsLabel, + defaultValue: null)); + properties.add(DiagnosticsProperty('focusNode', focusNode, + defaultValue: null)); + properties.add( + DiagnosticsProperty('style', style, defaultValue: null)); + properties.add( + DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); + properties.add(DiagnosticsProperty('showCursor', showCursor, + defaultValue: false)); + properties.add(IntProperty('minLines', minLines, defaultValue: null)); + properties.add(IntProperty('maxLines', maxLines, defaultValue: null)); + properties.add( + EnumProperty('textAlign', textAlign, defaultValue: null)); + properties.add(EnumProperty('textDirection', textDirection, + defaultValue: null)); + properties.add( + DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null)); + properties + .add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); + properties + .add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); + properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, + defaultValue: null)); + properties.add(DiagnosticsProperty('cursorColor', cursorColor, + defaultValue: null)); + properties.add(FlagProperty('selectionEnabled', + value: selectionEnabled, + defaultValue: true, + ifFalse: 'selection disabled')); + properties.add(DiagnosticsProperty( + 'selectionControls', selectionControls, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollPhysics', scrollPhysics, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'textHeightBehavior', textHeightBehavior, + defaultValue: null)); + } +} + +class _FlowySelectableTextState extends State + implements TextSelectionGestureDetectorBuilderDelegate { + EditableTextState? get _editableText => editableTextKey.currentState; + + late _TextSpanEditingController _controller; + + FocusNode? _focusNode; + FocusNode get _effectiveFocusNode => + widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true)); + + bool _showSelectionHandles = false; + + late _SelectableTextSelectionGestureDetectorBuilder + _selectionGestureDetectorBuilder; + + // API for TextSelectionGestureDetectorBuilderDelegate. + @override + late bool forcePressEnabled; + + @override + final GlobalKey editableTextKey = + GlobalKey(); + + @override + bool get selectionEnabled => widget.selectionEnabled; + // End of API for TextSelectionGestureDetectorBuilderDelegate. + + @override + void initState() { + super.initState(); + _selectionGestureDetectorBuilder = + _SelectableTextSelectionGestureDetectorBuilder(state: this); + _controller = _TextSpanEditingController( + textSpan: widget.textSpan ?? TextSpan(text: widget.data), + ); + _controller.addListener(_onControllerChanged); + } + + @override + void didUpdateWidget(FlowySelectableText oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.data != oldWidget.data || + widget.textSpan != oldWidget.textSpan) { + _controller.removeListener(_onControllerChanged); + _controller = _TextSpanEditingController( + textSpan: widget.textSpan ?? TextSpan(text: widget.data), + ); + _controller.addListener(_onControllerChanged); + } + if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) { + _showSelectionHandles = false; + } else { + _showSelectionHandles = true; + } + } + + @override + void dispose() { + _focusNode?.dispose(); + _controller.removeListener(_onControllerChanged); + super.dispose(); + } + + void _onControllerChanged() { + final bool showSelectionHandles = + !_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed; + if (showSelectionHandles == _showSelectionHandles) { + return; + } + setState(() { + _showSelectionHandles = showSelectionHandles; + }); + } + + TextSelection? _lastSeenTextSelection; + + void _handleSelectionChanged( + TextSelection selection, SelectionChangedCause? cause) { + final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); + if (willShowSelectionHandles != _showSelectionHandles) { + setState(() { + _showSelectionHandles = willShowSelectionHandles; + }); + } + // TODO(chunhtai): The selection may be the same. We should remove this + // check once this is fixed https://github.com/flutter/flutter/issues/76349. + if (widget.onSelectionChanged != null && + _lastSeenTextSelection != selection) { + widget.onSelectionChanged!(selection, cause); + } + _lastSeenTextSelection = selection; + + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + if (cause == SelectionChangedCause.longPress) { + _editableText?.bringIntoView(selection.base); + } + return; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + // Do nothing. + } + } + + /// Toggle the toolbar when a selection handle is tapped. + void _handleSelectionHandleTapped() { + if (_controller.selection.isCollapsed) { + _editableText!.toggleToolbar(); + } + } + + bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { + // When the text field is activated by something that doesn't trigger the + // selection overlay, we shouldn't show the handles either. + if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) + return false; + + if (_controller.selection.isCollapsed) return false; + + if (cause == SelectionChangedCause.keyboard) return false; + + if (cause == SelectionChangedCause.longPress) return true; + + if (_controller.text.isNotEmpty) return true; + + return false; + } + + @override + Widget build(BuildContext context) { + // TODO(garyq): Assert to block WidgetSpans from being used here are removed, + // but we still do not yet have nice handling of things like carets, clipboard, + // and other features. We should add proper support. Currently, caret handling + // is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010 + // should be landed in SkParagraph after the switch is complete. + assert(debugCheckHasMediaQuery(context)); + assert(debugCheckHasDirectionality(context)); + assert( + !(widget.style != null && + widget.style!.inherit == false && + (widget.style!.fontSize == null || + widget.style!.textBaseline == null)), + 'inherit false style must supply fontSize and textBaseline', + ); + + final ThemeData theme = Theme.of(context); + final TextSelectionThemeData selectionTheme = + TextSelectionTheme.of(context); + final FocusNode focusNode = _effectiveFocusNode; + + TextSelectionControls? textSelectionControls = widget.selectionControls; + final bool paintCursorAboveText; + final bool cursorOpacityAnimates; + Offset? cursorOffset; + Color? cursorColor = widget.cursorColor; + final Color selectionColor; + Radius? cursorRadius = widget.cursorRadius; + + switch (theme.platform) { + case TargetPlatform.iOS: + final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); + forcePressEnabled = true; + textSelectionControls ??= cupertinoTextSelectionControls; + paintCursorAboveText = true; + cursorOpacityAnimates = true; + cursorColor ??= + selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; + selectionColor = selectionTheme.selectionColor ?? + cupertinoTheme.primaryColor.withOpacity(0.40); + cursorRadius ??= const Radius.circular(2.0); + cursorOffset = Offset( + iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); + break; + + case TargetPlatform.macOS: + final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); + forcePressEnabled = false; + textSelectionControls ??= cupertinoDesktopTextSelectionControls; + paintCursorAboveText = true; + cursorOpacityAnimates = true; + cursorColor ??= + selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; + selectionColor = selectionTheme.selectionColor ?? + cupertinoTheme.primaryColor.withOpacity(0.40); + cursorRadius ??= const Radius.circular(2.0); + cursorOffset = Offset( + iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + forcePressEnabled = false; + textSelectionControls ??= materialTextSelectionControls; + paintCursorAboveText = false; + cursorOpacityAnimates = false; + cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; + selectionColor = selectionTheme.selectionColor ?? + theme.colorScheme.primary.withOpacity(0.40); + break; + + case TargetPlatform.linux: + case TargetPlatform.windows: + forcePressEnabled = false; + textSelectionControls ??= desktopTextSelectionControls; + paintCursorAboveText = false; + cursorOpacityAnimates = false; + cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; + selectionColor = selectionTheme.selectionColor ?? + theme.colorScheme.primary.withOpacity(0.40); + break; + } + + final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); + TextStyle? effectiveTextStyle = widget.style; + if (effectiveTextStyle == null || effectiveTextStyle.inherit) + effectiveTextStyle = defaultTextStyle.style.merge(widget.style); + if (MediaQuery.boldTextOverride(context)) + effectiveTextStyle = effectiveTextStyle + .merge(const TextStyle(fontWeight: FontWeight.bold)); + final Widget child = RepaintBoundary( + child: EditableText( + key: editableTextKey, + style: effectiveTextStyle, + readOnly: true, + textWidthBasis: + widget.textWidthBasis ?? defaultTextStyle.textWidthBasis, + textHeightBehavior: + widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior, + showSelectionHandles: _showSelectionHandles, + showCursor: widget.showCursor, + controller: _controller, + focusNode: focusNode, + strutStyle: widget.strutStyle ?? const StrutStyle(), + textAlign: + widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, + textDirection: widget.textDirection, + textScaleFactor: widget.textScaleFactor, + autofocus: widget.autofocus, + forceLine: false, + toolbarOptions: widget.toolbarOptions, + minLines: widget.minLines, + maxLines: widget.maxLines ?? defaultTextStyle.maxLines, + selectionColor: selectionColor, + selectionControls: + widget.selectionEnabled ? textSelectionControls : null, + onSelectionChanged: _handleSelectionChanged, + onSelectionHandleTapped: _handleSelectionHandleTapped, + rendererIgnoresPointer: true, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: cursorRadius, + cursorColor: cursorColor, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + cursorOpacityAnimates: cursorOpacityAnimates, + cursorOffset: cursorOffset, + paintCursorAboveText: paintCursorAboveText, + backgroundCursorColor: CupertinoColors.inactiveGray, + enableInteractiveSelection: widget.enableInteractiveSelection, + dragStartBehavior: widget.dragStartBehavior, + scrollPhysics: widget.scrollPhysics, + autofillHints: null, + ), + ); + + return Semantics( + label: widget.semanticsLabel, + excludeSemantics: widget.semanticsLabel != null, + onLongPress: () { + _effectiveFocusNode.requestFocus(); + }, + child: _selectionGestureDetectorBuilder.buildGestureDetector( + behavior: HitTestBehavior.translucent, + child: child, + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index df2039fbd0..0077707fe8 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'flowy_selectable_text.dart'; class TextNodeBuilder extends NodeWidgetBuilder { TextNodeBuilder.create({ @@ -37,23 +38,82 @@ class _TextNodeWidget extends StatefulWidget { State<_TextNodeWidget> createState() => __TextNodeWidgetState(); } -String _textContentOfDelta(Delta delta) { - return delta.operations.fold("", (previousValue, element) { - if (element is TextInsert) { - return previousValue + element.content; - } - return previousValue; - }); -} - class __TextNodeWidgetState extends State<_TextNodeWidget> implements DeltaTextInputClient { - final _focusNode = FocusNode(debugLabel: "input"); TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; TextInputConnection? _textInputConnection; + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySelectableText.rich( + node.toTextSpan(), + showCursor: true, + enableInteractiveSelection: true, + onSelectionChanged: _onSelectionChanged, + // autofocus: true, + focusNode: FocusNode( + onKey: _onKey, + ), + ), + if (node.children.isNotEmpty) + ...node.children.map( + (e) => editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: e, + editorState: editorState, + ), + ), + ), + const SizedBox( + height: 10, + ), + ], + ); + } + + KeyEventResult _onKey(FocusNode focusNode, RawKeyEvent event) { + if (event is RawKeyDownEvent) { + final sel = _globalSelectionToLocal(node, editorState.cursorSelection); + if (event.logicalKey == LogicalKeyboardKey.backspace) { + _backDeleteTextAtSelection(sel); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.delete) { + _forwardDeleteTextAtSelection(sel); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + } + + void _onSelectionChanged( + TextSelection selection, SelectionChangedCause? cause) { + _textInputConnection?.close(); + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + enableDeltaModel: true, + inputType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + ), + ); + debugPrint('selection: $selection'); + editorState.cursorSelection = _localSelectionToGlobal(node, selection); + _textInputConnection + ?..show() + ..setEditingState( + TextEditingValue( + text: node.toRawString(), + selection: selection, + ), + ); + } + _backDeleteTextAtSelection(TextSelection? sel) { if (sel == null) { return; @@ -94,78 +154,11 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> _setEditingStateFromGlobal() { _textInputConnection?.setEditingState(TextEditingValue( - text: node.toString(), + text: node.toRawString(), selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? const TextSelection.collapsed(offset: 0))); } - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - KeyboardListener( - focusNode: _focusNode, - onKeyEvent: ((value) { - if (value is KeyDownEvent || value is KeyRepeatEvent) { - final sel = - _globalSelectionToLocal(node, editorState.cursorSelection); - if (value.logicalKey.keyLabel == "Backspace") { - _backDeleteTextAtSelection(sel); - } else if (value.logicalKey.keyLabel == "Delete") { - _forwardDeleteTextAtSelection(sel); - } - } - }), - child: SelectableText.rich( - showCursor: true, - TextSpan( - children: node.toTextSpans(), - ), - onTap: () { - _focusNode.requestFocus(); - }, - onSelectionChanged: ((selection, cause) { - _textInputConnection?.close(); - _textInputConnection = TextInput.attach( - this, - const TextInputConfiguration( - enableDeltaModel: true, - inputType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - ), - ); - debugPrint('selection: $selection'); - editorState.cursorSelection = - _localSelectionToGlobal(node, selection); - _textInputConnection - ?..show() - ..setEditingState( - TextEditingValue( - text: node.toString(), - selection: selection, - ), - ); - }), - ), - ), - if (node.children.isNotEmpty) - ...node.children.map( - (e) => editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: e, - editorState: editorState, - ), - ), - ), - const SizedBox( - height: 10, - ), - ], - ); - } - @override void connectionClosed() { // TODO: implement connectionClosed @@ -178,7 +171,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override // TODO: implement currentTextEditingValue TextEditingValue? get currentTextEditingValue => TextEditingValue( - text: node.toString(), + text: node.toRawString(), selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? const TextSelection.collapsed(offset: 0)); @@ -241,10 +234,11 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> } extension on TextNode { - List toTextSpans() => delta.operations - .whereType() - .map((op) => op.toTextSpan()) - .toList(); + TextSpan toTextSpan() => TextSpan( + children: delta.operations + .whereType() + .map((op) => op.toTextSpan()) + .toList()); } extension on TextInsert { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 9d8b9906b0..58f32d31c0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -184,10 +184,8 @@ class TextNode extends Node { return map; } - String toString() { - _delta.operations - .whereType() - .map((op) => op.content) - .toString(); - } + String toRawString() => _delta.operations + .whereType() + .map((op) => op.content) + .toString(); } From f4bbe776122bb173adb9604ecc47a0b5397337cb Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 20 Jul 2022 16:04:35 +0800 Subject: [PATCH 043/121] feat: undo manager --- .../example/lib/plugin/text_node_widget.dart | 11 ++ .../flowy_editor/lib/editor_state.dart | 55 +++++++-- .../lib/operation/transaction_builder.dart | 4 +- .../flowy_editor/lib/undo_manager.dart | 111 ++++++++++++++++++ 4 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 0077707fe8..b81ffca0ab 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -42,6 +42,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> implements DeltaTextInputClient { TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; + bool _metaKeyDown = false; TextInputConnection? _textInputConnection; @@ -86,6 +87,16 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> } else if (event.logicalKey == LogicalKeyboardKey.delete) { _forwardDeleteTextAtSelection(sel); return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.metaLeft || + event.logicalKey == LogicalKeyboardKey.metaRight) { + _metaKeyDown = true; + } else if (event.logicalKey == LogicalKeyboardKey.keyZ && _metaKeyDown) { + editorState.undoManager.undo(); + } + } else if (event is RawKeyUpEvent) { + if (event.logicalKey == LogicalKeyboardKey.metaLeft || + event.logicalKey == LogicalKeyboardKey.metaRight) { + _metaKeyDown = false; } } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 521231a495..44c36f7d16 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,23 +1,31 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/operation/operation.dart'; -import 'package:flowy_editor/document/attributes.dart'; +import 'dart:async'; +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/undo_manager.dart'; import 'package:flutter/material.dart'; -import './document/state_tree.dart'; import './document/selection.dart'; -import './operation/operation.dart'; -import './operation/transaction.dart'; -import './render/render_plugins.dart'; + +class ApplyOptions { + final bool noLog; + const ApplyOptions({ + this.noLog = false, + }); +} class EditorState { final StateTree document; final RenderPlugins renderPlugins; + final UndoManager undoManager = UndoManager(); Selection? cursorSelection; + Timer? _debouncedSealHistoryItemTimer; + EditorState({ required this.document, required this.renderPlugins, - }); + }) { + undoManager.state = this; + } /// TODO: move to a better place. Widget build(BuildContext context) { @@ -30,14 +38,41 @@ class EditorState { ); } - void apply(Transaction transaction) { + apply(Transaction transaction, + [ApplyOptions options = const ApplyOptions()]) { for (final op in transaction.operations) { _applyOperation(op); } cursorSelection = transaction.afterSelection; + + if (options.noLog) { + return; + } + + final undoItem = undoManager.getUndoHistoryItem(); + undoItem.addAll(transaction.operations); + if (undoItem.beforeSelection == null && + transaction.beforeSelection != null) { + undoItem.beforeSelection = transaction.beforeSelection; + } + undoItem.afterSelection = transaction.afterSelection; + + _debouncedSealHistoryItem(); } - void _applyOperation(Operation op) { + _debouncedSealHistoryItem() { + _debouncedSealHistoryItemTimer?.cancel(); + _debouncedSealHistoryItemTimer = + Timer(const Duration(milliseconds: 1000), () { + if (undoManager.undoStack.isNonEmpty) { + debugPrint('Seal history item'); + final last = undoManager.undoStack.last; + last.seal(); + } + }); + } + + _applyOperation(Operation op) { if (op is InsertOperation) { document.insert(op.path, op.value); } else if (op is UpdateOperation) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index ec088dc25d..6fca48f230 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -31,7 +31,7 @@ class TransactionBuilder { /// Commit the operations to the state commit() { - final transaction = _finish(); + final transaction = finish(); state.apply(transaction); } @@ -99,7 +99,7 @@ class TransactionBuilder { operations.add(op); } - Transaction _finish() { + Transaction finish() { return Transaction( operations: UnmodifiableListView(operations), beforeSelection: beforeSelection, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart new file mode 100644 index 0000000000..b523ba54a7 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart @@ -0,0 +1,111 @@ +import 'dart:collection'; + +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/operation/transaction.dart'; +import 'package:flowy_editor/editor_state.dart'; + +class HistoryItem extends LinkedListEntry { + final List operations = []; + Selection? beforeSelection; + Selection? afterSelection; + bool _sealed = false; + + HistoryItem(); + + seal() { + _sealed = true; + } + + add(Operation op) { + operations.add(op); + } + + addAll(Iterable iterable) { + operations.addAll(iterable); + } + + bool get sealed { + return _sealed; + } + + Transaction toTransaction(EditorState state) { + final builder = TransactionBuilder(state); + for (var i = operations.length - 1; i >= 0; i--) { + final operation = operations[i]; + final inverted = operation.invert(); + builder.add(inverted); + } + builder.afterSelection = beforeSelection; + builder.beforeSelection = afterSelection; + return builder.finish(); + } +} + +class FixedSizeStack { + final _list = LinkedList(); + final int maxSize; + + FixedSizeStack(this.maxSize); + + push(HistoryItem stackItem) { + if (_list.length >= maxSize) { + _list.remove(_list.first); + } + _list.add(stackItem); + } + + HistoryItem? pop() { + if (_list.isEmpty) { + return null; + } + final last = _list.last; + + _list.remove(last); + + return last; + } + + HistoryItem get last { + return _list.last; + } + + bool get isEmpty { + return _list.isEmpty; + } + + bool get isNonEmpty { + return _list.isNotEmpty; + } +} + +class UndoManager { + final undoStack = FixedSizeStack(20); + final redoStack = FixedSizeStack(20); + EditorState? state; + + HistoryItem getUndoHistoryItem() { + if (undoStack.isEmpty) { + final item = HistoryItem(); + undoStack.push(item); + return item; + } + final last = undoStack.last; + if (last.sealed) { + final item = HistoryItem(); + undoStack.push(item); + return item; + } + return last; + } + + undo() { + final historyItem = undoStack.pop(); + if (historyItem == null) { + return; + } + final transaction = historyItem.toTransaction(state!); + state!.apply(transaction, const ApplyOptions(noLog: true)); + } +} From 0626912a4c2b2ab7c6cdcc10e42064e585d6bf59 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 21 Jul 2022 10:57:48 +0800 Subject: [PATCH 044/121] feat: add comment and fix issues --- .../flowy_editor/lib/editor_state.dart | 28 +++++++------- .../flowy_editor/lib/undo_manager.dart | 37 ++++++++++--------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 44c36f7d16..1e6e404385 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -6,9 +6,12 @@ import 'package:flutter/material.dart'; import './document/selection.dart'; class ApplyOptions { - final bool noLog; + /// This flag indicates that + /// whether the transaction should be recorded into + /// the undo stack. + final bool recordUndo; const ApplyOptions({ - this.noLog = false, + this.recordUndo = true, }); } @@ -45,19 +48,16 @@ class EditorState { } cursorSelection = transaction.afterSelection; - if (options.noLog) { - return; + if (options.recordUndo) { + final undoItem = undoManager.getUndoHistoryItem(); + undoItem.addAll(transaction.operations); + if (undoItem.beforeSelection == null && + transaction.beforeSelection != null) { + undoItem.beforeSelection = transaction.beforeSelection; + } + undoItem.afterSelection = transaction.afterSelection; + _debouncedSealHistoryItem(); } - - final undoItem = undoManager.getUndoHistoryItem(); - undoItem.addAll(transaction.operations); - if (undoItem.beforeSelection == null && - transaction.beforeSelection != null) { - undoItem.beforeSelection = transaction.beforeSelection; - } - undoItem.afterSelection = transaction.afterSelection; - - _debouncedSealHistoryItem(); } _debouncedSealHistoryItem() { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart index b523ba54a7..e85cebe54a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart @@ -6,6 +6,9 @@ import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/operation/transaction.dart'; import 'package:flowy_editor/editor_state.dart'; +/// This class contains operations to committed by users. +/// If a [HistoryItem] is not sealed, operations can be added sequentially. +/// Otherwise, the operations should be added to a new [HistoryItem]. class HistoryItem extends LinkedListEntry { final List operations = []; Selection? beforeSelection; @@ -18,6 +21,8 @@ class HistoryItem extends LinkedListEntry { _sealed = true; } + bool get sealed => _sealed; + add(Operation op) { operations.add(op); } @@ -26,10 +31,6 @@ class HistoryItem extends LinkedListEntry { operations.addAll(iterable); } - bool get sealed { - return _sealed; - } - Transaction toTransaction(EditorState state) { final builder = TransactionBuilder(state); for (var i = operations.length - 1; i >= 0; i--) { @@ -67,24 +68,22 @@ class FixedSizeStack { return last; } - HistoryItem get last { - return _list.last; - } + HistoryItem get last => _list.last; - bool get isEmpty { - return _list.isEmpty; - } + bool get isEmpty => _list.isEmpty; - bool get isNonEmpty { - return _list.isNotEmpty; - } + bool get isNonEmpty => _list.isNotEmpty; } class UndoManager { - final undoStack = FixedSizeStack(20); - final redoStack = FixedSizeStack(20); + final FixedSizeStack undoStack; + final FixedSizeStack redoStack; EditorState? state; + UndoManager([int stackSize = 20]) + : undoStack = FixedSizeStack(stackSize), + redoStack = FixedSizeStack(stackSize); + HistoryItem getUndoHistoryItem() { if (undoStack.isEmpty) { final item = HistoryItem(); @@ -101,11 +100,15 @@ class UndoManager { } undo() { + final s = state; + if (s == null) { + return; + } final historyItem = undoStack.pop(); if (historyItem == null) { return; } - final transaction = historyItem.toTransaction(state!); - state!.apply(transaction, const ApplyOptions(noLog: true)); + final transaction = historyItem.toTransaction(s); + s.apply(transaction, const ApplyOptions(recordUndo: false)); } } From 7ae153f5daf627cc8d83d29926c5420d4cdd793a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 21 Jul 2022 11:47:41 +0800 Subject: [PATCH 045/121] fix(typo): comments of UndoManager --- frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart index e85cebe54a..d01ffa351a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart @@ -6,7 +6,7 @@ import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/operation/transaction.dart'; import 'package:flowy_editor/editor_state.dart'; -/// This class contains operations to committed by users. +/// This class contains operations committed by users. /// If a [HistoryItem] is not sealed, operations can be added sequentially. /// Otherwise, the operations should be added to a new [HistoryItem]. class HistoryItem extends LinkedListEntry { From e2f35dd5ccb4c51109684a8255b72b1e3edec361 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 21 Jul 2022 14:55:37 +0800 Subject: [PATCH 046/121] feat: support selection overlay --- .../flowy_editor/example/.vscode/launch.json | 25 ++ .../flowy_editor/example/lib/main.dart | 3 +- .../lib/plugin/debuggable_rich_text.dart | 102 ++++++++ .../lib/plugin/document_node_widget.dart | 57 ++++- .../example/lib/plugin/image_node_widget.dart | 21 +- .../lib/plugin/selected_text_node_widget.dart | 223 ++++++++++++++++++ .../example/lib/plugin/text_node_widget.dart | 3 +- .../text_with_check_box_node_widget.dart | 1 + .../plugin/text_with_heading_node_widget.dart | 5 +- .../flowy_editor/lib/document/node.dart | 2 + .../flowy_editor/lib/editor_state.dart | 84 ++++++- .../flowy_editor/lib/flowy_editor.dart | 2 + .../lib/render/node_widget_builder.dart | 19 +- .../lib/render/render_plugins.dart | 4 + .../flowy_editor/lib/render/selectable.dart | 8 + 15 files changed, 532 insertions(+), 27 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json b/frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json new file mode 100644 index 0000000000..091adbfb6b --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "example", + "request": "launch", + "type": "dart" + }, + { + "name": "example (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "example (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 8a413f78b1..83bc2044e0 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:example/plugin/document_node_widget.dart'; +import 'package:example/plugin/selected_text_node_widget.dart'; import 'package:example/plugin/text_with_heading_node_widget.dart'; import 'package:example/plugin/image_node_widget.dart'; import 'package:example/plugin/text_node_widget.dart'; @@ -65,7 +66,7 @@ class _MyHomePageState extends State { renderPlugins ..register('editor', EditorNodeWidgetBuilder.create) - ..register('text', TextNodeBuilder.create) + ..register('text', SelectedTextNodeBuilder.create) ..register('image', ImageNodeBuilder.create) ..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create) ..register('text/with-heading', TextWithHeadingNodeBuilder.create); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart new file mode 100644 index 0000000000..6028774ba9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart @@ -0,0 +1,102 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class DebuggableRichText extends StatefulWidget { + final InlineSpan text; + final GlobalKey textKey; + + const DebuggableRichText({ + Key? key, + required this.text, + required this.textKey, + }) : super(key: key); + + @override + State createState() => _DebuggableRichTextState(); +} + +class _DebuggableRichTextState extends State { + final List _textRects = []; + + RenderParagraph get _renderParagraph => + widget.textKey.currentContext?.findRenderObject() as RenderParagraph; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _updateTextRects(); + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + CustomPaint( + painter: _BoxPainter( + rects: _textRects, + ), + ), + RichText( + key: widget.textKey, + text: widget.text, + ), + ], + ); + } + + void _updateTextRects() { + setState(() { + _textRects + ..clear() + ..addAll( + _computeLocalSelectionRects( + TextSelection( + baseOffset: 0, + extentOffset: widget.text.toPlainText().length, + ), + ), + ); + }); + } + + List _computeLocalSelectionRects(TextSelection selection) { + final textBoxes = _renderParagraph.getBoxesForSelection(selection); + return textBoxes.map((box) => box.toRect()).toList(); + } +} + +class _BoxPainter extends CustomPainter { + final List _rects; + final Paint _paint; + + _BoxPainter({ + required List rects, + bool fill = false, + }) : _rects = rects, + _paint = Paint() { + _paint.style = fill ? PaintingStyle.fill : PaintingStyle.stroke; + } + + @override + void paint(Canvas canvas, Size size) { + for (final rect in _rects) { + canvas.drawRect( + rect, + _paint + ..color = Color( + (Random().nextDouble() * 0xFFFFFF).toInt(), + ).withOpacity(1.0), + ); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart index 2de62948d5..80ca4f5f00 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart @@ -1,15 +1,18 @@ import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; class EditorNodeWidgetBuilder extends NodeWidgetBuilder { EditorNodeWidgetBuilder.create({ required super.editorState, required super.node, + required super.key, }) : super.create(); @override Widget build(BuildContext buildContext) { return SingleChildScrollView( + key: key, child: _EditorNodeWidget( node: node, editorState: editorState, @@ -30,21 +33,49 @@ class _EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: e, - editorState: editorState, + return RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: { + PanGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => PanGestureRecognizer(), + (recognizer) { + recognizer + ..onStart = _onPanStart + ..onUpdate = _onPanUpdate + ..onEnd = _onPanEnd; + }, + ), + }, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: e, + editorState: editorState, + ), ), - ), - ) - .toList(), + ) + .toList(), + ), ), ); } + + void _onPanStart(DragStartDetails details) { + editorState.panStartOffset = details.globalPosition; + } + + void _onPanUpdate(DragUpdateDetails details) { + editorState.panEndOffset = details.globalPosition; + editorState.updateSelection(); + } + + void _onPanEnd(DragEndDetails details) { + // do nothing + } } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 692d00baf2..143f5aff01 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -5,18 +5,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder { ImageNodeBuilder.create({ required super.node, required super.editorState, + required super.key, }) : super.create(); @override Widget build(BuildContext buildContext) { return _ImageNodeWidget( + key: key, node: node, editorState: editorState, ); } } -class _ImageNodeWidget extends StatelessWidget { +class _ImageNodeWidget extends StatefulWidget { final Node node; final EditorState editorState; @@ -26,7 +28,22 @@ class _ImageNodeWidget extends StatelessWidget { required this.editorState, }) : super(key: key); - String get src => node.attributes['image_src'] as String; + @override + State<_ImageNodeWidget> createState() => __ImageNodeWidgetState(); +} + +class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { + Node get node => widget.node; + EditorState get editorState => widget.editorState; + String get src => widget.node.attributes['image_src'] as String; + + @override + List getOverlayRectsInRange(Offset start, Offset end) { + final renderBox = context.findRenderObject() as RenderBox; + final size = renderBox.size; + final boxOffset = renderBox.localToGlobal(Offset.zero); + return [boxOffset & size]; + } @override Widget build(BuildContext context) { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart new file mode 100644 index 0000000000..59b85bb33b --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -0,0 +1,223 @@ +import 'package:example/plugin/debuggable_rich_text.dart'; +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class SelectedTextNodeBuilder extends NodeWidgetBuilder { + SelectedTextNodeBuilder.create({ + required super.node, + required super.editorState, + required super.key, + }) : super.create() { + nodeValidator = ((node) { + return node.type == 'text'; + }); + } + + @override + Widget build(BuildContext buildContext) { + return _SelectedTextNodeWidget( + key: key, + node: node, + editorState: editorState, + ); + } +} + +class _SelectedTextNodeWidget extends StatefulWidget { + final Node node; + final EditorState editorState; + + const _SelectedTextNodeWidget({ + Key? key, + required this.node, + required this.editorState, + }) : super(key: key); + + @override + State<_SelectedTextNodeWidget> createState() => + _SelectedTextNodeWidgetState(); +} + +class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> + with Selectable { + TextNode get node => widget.node as TextNode; + EditorState get editorState => widget.editorState; + + final _textKey = GlobalKey(); + + RenderParagraph get _renderParagraph => + _textKey.currentContext?.findRenderObject() as RenderParagraph; + + @override + List getOverlayRectsInRange(Offset start, Offset end) { + // Returns select all if the start or end exceeds the size of the box + // TODO: don't need to compute everytime. + var rects = _computeSelectionRects( + TextSelection(baseOffset: 0, extentOffset: node.toRawString().length), + ); + + if (end.dy > start.dy) { + // downward + if (end.dy >= rects.last.bottom) { + return rects; + } + } else { + // upward + if (end.dy <= rects.first.top) { + return rects; + } + } + + final selectionBaseOffset = _getTextPositionAtOffset(start).offset; + final selectionExtentOffset = _getTextPositionAtOffset(end).offset; + final textSelection = TextSelection( + baseOffset: selectionBaseOffset, + extentOffset: selectionExtentOffset, + ); + return _computeSelectionRects(textSelection); + } + + @override + Widget build(BuildContext context) { + Widget richText; + if (kDebugMode) { + richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey); + } else { + richText = RichText(key: _textKey, text: node.toTextSpan()); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + richText, + if (node.children.isNotEmpty) + ...node.children.map( + (e) => editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: e, + editorState: editorState, + ), + ), + ), + const SizedBox( + height: 5, + ), + ], + ); + } + + TextPosition _getTextPositionAtOffset(Offset offset) { + final textOffset = _renderParagraph.globalToLocal(offset); + return _renderParagraph.getPositionForOffset(textOffset); + } + + List _computeSelectionRects(TextSelection selection) { + final textBoxes = _renderParagraph.getBoxesForSelection(selection); + return textBoxes + .map((box) => + _renderParagraph.localToGlobal(box.toRect().topLeft) & + box.toRect().size) + .toList(); + } +} + +extension on TextNode { + TextSpan toTextSpan() => TextSpan( + children: delta.operations + .whereType() + .map((op) => op.toTextSpan()) + .toList()); +} + +extension on TextInsert { + TextSpan toTextSpan() { + FontWeight? fontWeight; + FontStyle? fontStyle; + TextDecoration? decoration; + GestureRecognizer? gestureRecognizer; + Color color = Colors.black; + Color highLightColor = Colors.transparent; + double fontSize = 16.0; + final attributes = this.attributes; + if (attributes?['bold'] == true) { + fontWeight = FontWeight.bold; + } + if (attributes?['italic'] == true) { + fontStyle = FontStyle.italic; + } + if (attributes?['underline'] == true) { + decoration = TextDecoration.underline; + } + if (attributes?['strikethrough'] == true) { + decoration = TextDecoration.lineThrough; + } + if (attributes?['highlight'] is String) { + highLightColor = Color(int.parse(attributes!['highlight'])); + } + if (attributes?['href'] is String) { + color = const Color.fromARGB(255, 55, 120, 245); + decoration = TextDecoration.underline; + gestureRecognizer = TapGestureRecognizer() + ..onTap = () { + launchUrlString(attributes?['href']); + }; + } + final heading = attributes?['heading'] as String?; + if (heading != null) { + // TODO: make it better + if (heading == 'h1') { + fontSize = 30.0; + } else if (heading == 'h2') { + fontSize = 20.0; + } + fontWeight = FontWeight.bold; + } + return TextSpan( + text: content, + style: TextStyle( + fontWeight: fontWeight, + fontStyle: fontStyle, + decoration: decoration, + color: color, + fontSize: fontSize, + backgroundColor: highLightColor, + ), + recognizer: gestureRecognizer, + ); + } +} + +class FlowyPainter extends CustomPainter { + final List _rects; + final Paint _paint; + + FlowyPainter({ + Key? key, + required Color color, + required List rects, + bool fill = false, + }) : _rects = rects, + _paint = Paint()..color = color { + _paint.style = fill ? PaintingStyle.fill : PaintingStyle.stroke; + } + + @override + void paint(Canvas canvas, Size size) { + for (final rect in _rects) { + canvas.drawRect( + rect, + _paint, + ); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 0077707fe8..911a6179be 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -12,6 +12,7 @@ class TextNodeBuilder extends NodeWidgetBuilder { TextNodeBuilder.create({ required super.node, required super.editorState, + required super.key, }) : super.create() { nodeValidator = ((node) { return node.type == 'text'; @@ -20,7 +21,7 @@ class TextNodeBuilder extends NodeWidgetBuilder { @override Widget build(BuildContext buildContext) { - return _TextNodeWidget(node: node, editorState: editorState); + return _TextNodeWidget(key: key, node: node, editorState: editorState); } } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart index 37a30fb6be..ff6c6e9932 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart @@ -5,6 +5,7 @@ class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder { TextWithCheckBoxNodeBuilder.create({ required super.node, required super.editorState, + required super.key, }) : super.create(); // TODO: check the type diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart index 9519e130f2..22022a65ec 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart @@ -5,6 +5,7 @@ class TextWithHeadingNodeBuilder extends NodeWidgetBuilder { TextWithHeadingNodeBuilder.create({ required super.editorState, required super.node, + required super.key, }) : super.create() { nodeValidator = (node) => node.attributes.containsKey('heading'); } @@ -15,9 +16,9 @@ class TextWithHeadingNodeBuilder extends NodeWidgetBuilder { return const Padding( padding: EdgeInsets.only(top: 10), ); - } else if (heading == 'h1') { + } else if (heading == 'h2') { return const Padding( - padding: EdgeInsets.only(top: 10), + padding: EdgeInsets.only(top: 5), ); } return const Padding( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 58f32d31c0..8c75eca360 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -10,6 +10,8 @@ class Node extends ChangeNotifier with LinkedListEntry { final LinkedList children; final Attributes attributes; + GlobalKey? key; + String? get subtype { // TODO: make 'subtype' as a const value. if (attributes.containsKey('subtype')) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 521231a495..bc111ab4b8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,6 +1,6 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/operation/operation.dart'; -import 'package:flowy_editor/document/attributes.dart'; +import 'package:flowy_editor/render/selectable.dart'; import 'package:flutter/material.dart'; import './document/state_tree.dart'; @@ -12,6 +12,10 @@ import './render/render_plugins.dart'; class EditorState { final StateTree document; final RenderPlugins renderPlugins; + + Offset? panStartOffset; + Offset? panEndOffset; + Selection? cursorSelection; EditorState({ @@ -48,4 +52,82 @@ class EditorState { document.textEdit(op.path, op.delta); } } + + List selectionOverlays = []; + + void updateSelection() { + final selectedNodes = _selectedNodes; + if (selectedNodes.isEmpty) { + return; + } + + assert(panStartOffset != null && panEndOffset != null); + + selectionOverlays + ..forEach((element) => element.remove()) + ..clear(); + for (final node in selectedNodes) { + final key = node.key; + if (key != null && key.currentState is Selectable) { + final selectable = key.currentState as Selectable; + final overlayRects = + selectable.getOverlayRectsInRange(panStartOffset!, panEndOffset!); + for (final rect in overlayRects) { + // TODO: refactor overlay implement. + final overlay = OverlayEntry(builder: ((context) { + return Positioned.fromRect( + rect: rect, + child: Container( + color: Colors.yellow.withAlpha(100), + ), + ); + })); + selectionOverlays.add(overlay); + Overlay.of(selectable.context)?.insert(overlay); + } + } + } + } + + List get _selectedNodes { + if (panStartOffset == null || panEndOffset == null) { + return []; + } + return _calculateSelectedNodes( + document.root, panStartOffset!, panEndOffset!); + } + + List _calculateSelectedNodes(Node node, Offset start, Offset end) { + List result = []; + + /// Skip the node without parent because it is the topmost node. + /// Skip the node without key because it cannot get the [RenderObject]. + if (node.parent != null && node.key != null) { + if (_isNodeInRange(node, start, end)) { + result.add(node); + } + } + + /// + for (final child in node.children) { + result.addAll(_calculateSelectedNodes(child, start, end)); + } + + return result; + } + + bool _isNodeInRange(Node node, Offset start, Offset end) { + assert(node.key != null); + final renderBox = + node.key?.currentContext?.findRenderObject() as RenderBox?; + + /// Return false directly if the [RenderBox] cannot found. + if (renderBox == null) { + return false; + } + + final rect = Rect.fromPoints(start, end); + final boxOffset = renderBox.localToGlobal(Offset.zero); + return rect.overlaps(boxOffset & renderBox.size); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index f816778603..f98e1b71b1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -3,8 +3,10 @@ library flowy_editor; export 'package:flowy_editor/document/state_tree.dart'; export 'package:flowy_editor/document/node.dart'; export 'package:flowy_editor/document/path.dart'; +export 'package:flowy_editor/document/text_delta.dart'; export 'package:flowy_editor/render/render_plugins.dart'; export 'package:flowy_editor/render/node_widget_builder.dart'; +export 'package:flowy_editor/render/selectable.dart'; export 'package:flowy_editor/operation/transaction.dart'; export 'package:flowy_editor/operation/transaction_builder.dart'; export 'package:flowy_editor/operation/operation.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index badce60694..f349a0fe3d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -9,6 +9,7 @@ typedef NodeValidator = bool Function(T node); class NodeWidgetBuilder { final EditorState editorState; final T node; + final Key key; bool rebuildOnNodeChanged; NodeValidator? nodeValidator; @@ -18,14 +19,22 @@ class NodeWidgetBuilder { NodeWidgetBuilder.create({ required this.editorState, required this.node, + required this.key, this.rebuildOnNodeChanged = true, }); /// Render the current [Node] /// and the layout style of [Node.Children]. - Widget build(BuildContext buildContext) => throw UnimplementedError(); + Widget build( + BuildContext buildContext, + ) => + throw UnimplementedError(); - Widget call(BuildContext buildContext) { + /// TODO: refactore this part. + /// return widget embeded with ChangeNotifier and widget itself. + Widget call( + BuildContext buildContext, + ) { /// TODO: Validate the node /// if failed, stop call build function, /// return Empty widget, and throw Error. @@ -34,11 +43,7 @@ class NodeWidgetBuilder { 'Node validate failure, node = { type: ${node.type}, attributes: ${node.attributes} }'); } - if (rebuildOnNodeChanged) { - return _buildNodeChangeNotifier(buildContext); - } else { - return build(buildContext); - } + return _buildNodeChangeNotifier(buildContext); } Widget _buildNodeChangeNotifier(BuildContext buildContext) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart index a9bbd8b070..efe5865d64 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -19,6 +19,7 @@ typedef NodeWidgetBuilderF = A Function({ required T node, required EditorState editorState, + required GlobalKey key, }); // unused @@ -63,9 +64,12 @@ class RenderPlugins { name += '/${node.subtype}'; } final nodeWidgetBuilder = _nodeWidgetBuilder(name); + final key = GlobalKey(); + node.key = key; return nodeWidgetBuilder( node: context.node, editorState: context.editorState, + key: key, )(context.buildContext); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart new file mode 100644 index 0000000000..89991bf687 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +/// +mixin Selectable on State { + /// Returns a [Rect] list for overlay. + /// [start] and [end] are global offsets. + List getOverlayRectsInRange(Offset start, Offset end); +} From e3e1d254946a4eb47819ba540b5dc81ed4255753 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 21 Jul 2022 15:21:19 +0800 Subject: [PATCH 047/121] feat: hide overlay when tap on the editor --- .../example/lib/plugin/document_node_widget.dart | 14 ++++++++++++++ .../packages/flowy_editor/lib/editor_state.dart | 7 ++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart index 80ca4f5f00..a9d891cf3d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart @@ -46,6 +46,13 @@ class _EditorNodeWidget extends StatelessWidget { ..onEnd = _onPanEnd; }, ), + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (recongizer) { + recongizer..onTap = _onTap; + }, + ) }, child: SingleChildScrollView( child: Column( @@ -66,8 +73,15 @@ class _EditorNodeWidget extends StatelessWidget { ); } + void _onTap() { + editorState.panStartOffset = null; + editorState.panEndOffset = null; + editorState.updateSelection(); + } + void _onPanStart(DragStartDetails details) { editorState.panStartOffset = details.globalPosition; + editorState.updateSelection(); } void _onPanUpdate(DragUpdateDetails details) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index bc111ab4b8..fce1ac8ffd 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -56,6 +56,10 @@ class EditorState { List selectionOverlays = []; void updateSelection() { + selectionOverlays + ..forEach((element) => element.remove()) + ..clear(); + final selectedNodes = _selectedNodes; if (selectedNodes.isEmpty) { return; @@ -63,9 +67,6 @@ class EditorState { assert(panStartOffset != null && panEndOffset != null); - selectionOverlays - ..forEach((element) => element.remove()) - ..clear(); for (final node in selectedNodes) { final key = node.key; if (key != null && key.currentState is Selectable) { From d200371002758540eb95934538bf480b7b582940 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 21 Jul 2022 17:56:56 +0800 Subject: [PATCH 048/121] feat: add keyboard and cursor --- .../flowy_editor/example/assets/document.json | 6 -- .../lib/plugin/document_node_widget.dart | 7 +- .../example/lib/plugin/image_node_widget.dart | 25 ++++- .../lib/plugin/selected_text_node_widget.dart | 58 ++++++++++- .../flowy_editor/lib/editor_state.dart | 96 ++++++++++++++++--- .../packages/flowy_editor/lib/keyboard.dart | 45 +++++++++ .../flowy_editor/lib/render/selectable.dart | 7 ++ .../lib/render/selection_overlay.dart | 0 8 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/selection_overlay.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index c2ba9fbb09..f74672345f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -3,12 +3,6 @@ "type": "editor", "attributes": {}, "children": [ - { - "type": "image", - "attributes": { - "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=1500w" - } - }, { "type": "text", "delta": [ diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart index a9d891cf3d..4608f8b20c 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart @@ -50,7 +50,7 @@ class _EditorNodeWidget extends StatelessWidget { GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(), (recongizer) { - recongizer..onTap = _onTap; + recongizer..onTapDown = _onTapDown; }, ) }, @@ -73,10 +73,13 @@ class _EditorNodeWidget extends StatelessWidget { ); } - void _onTap() { + void _onTapDown(TapDownDetails details) { editorState.panStartOffset = null; editorState.panEndOffset = null; editorState.updateSelection(); + + editorState.tapOffset = details.globalPosition; + editorState.updateCursor(); } void _onPanStart(DragStartDetails details) { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 143f5aff01..308477e294 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,5 +1,6 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class ImageNodeBuilder extends NodeWidgetBuilder { ImageNodeBuilder.create({ @@ -32,7 +33,8 @@ class _ImageNodeWidget extends StatefulWidget { State<_ImageNodeWidget> createState() => __ImageNodeWidgetState(); } -class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { +class __ImageNodeWidgetState extends State<_ImageNodeWidget> + with Selectable, KeyboardEventsRespondable { Node get node => widget.node; EditorState get editorState => widget.editorState; String get src => widget.node.attributes['image_src'] as String; @@ -45,6 +47,27 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { return [boxOffset & size]; } + @override + Rect getCursorRect(Offset start) { + final renderBox = context.findRenderObject() as RenderBox; + final size = Size(5, renderBox.size.height); + final boxOffset = renderBox.localToGlobal(Offset.zero); + final cursorOffset = + Offset(renderBox.size.width + boxOffset.dx, boxOffset.dy); + return cursorOffset & size; + } + + @override + KeyEventResult onKeyDown(RawKeyEvent event) { + if (event.logicalKey == LogicalKeyboardKey.backspace) { + TransactionBuilder(editorState) + ..deleteNode(node) + ..commit(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + @override Widget build(BuildContext context) { return _build(context); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 59b85bb33b..4d04bfb660 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SelectedTextNodeBuilder extends NodeWidgetBuilder { @@ -43,22 +44,24 @@ class _SelectedTextNodeWidget extends StatefulWidget { } class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> - with Selectable { + with Selectable, KeyboardEventsRespondable { TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; final _textKey = GlobalKey(); + TextSelection? _textSelection; RenderParagraph get _renderParagraph => _textKey.currentContext?.findRenderObject() as RenderParagraph; @override List getOverlayRectsInRange(Offset start, Offset end) { + var textSelection = + TextSelection(baseOffset: 0, extentOffset: node.toRawString().length); // Returns select all if the start or end exceeds the size of the box // TODO: don't need to compute everytime. - var rects = _computeSelectionRects( - TextSelection(baseOffset: 0, extentOffset: node.toRawString().length), - ); + var rects = _computeSelectionRects(textSelection); + _textSelection = textSelection; if (end.dy > start.dy) { // downward @@ -74,13 +77,44 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> final selectionBaseOffset = _getTextPositionAtOffset(start).offset; final selectionExtentOffset = _getTextPositionAtOffset(end).offset; - final textSelection = TextSelection( + textSelection = TextSelection( baseOffset: selectionBaseOffset, extentOffset: selectionExtentOffset, ); + _textSelection = textSelection; return _computeSelectionRects(textSelection); } + @override + Rect getCursorRect(Offset start) { + final selectionBaseOffset = _getTextPositionAtOffset(start).offset; + final textSelection = TextSelection.collapsed(offset: selectionBaseOffset); + _textSelection = textSelection; + return _computeCursorRect(textSelection.baseOffset); + } + + @override + KeyEventResult onKeyDown(RawKeyEvent event) { + if (event.logicalKey == LogicalKeyboardKey.backspace) { + final textSelection = _textSelection; + // TODO: just handle upforward delete. + if (textSelection != null) { + if (textSelection.isCollapsed) { + TransactionBuilder(editorState) + ..deleteText(node, textSelection.start - 1, 1) + ..commit(); + } else { + TransactionBuilder(editorState) + ..deleteText(node, textSelection.start, + textSelection.baseOffset - textSelection.extentOffset) + ..commit(); + } + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + @override Widget build(BuildContext context) { Widget richText; @@ -124,6 +158,20 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> box.toRect().size) .toList(); } + + Rect _computeCursorRect(int offset) { + final position = TextPosition(offset: offset); + var cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); + cursorOffset = _renderParagraph.localToGlobal(cursorOffset); + final cursorHeight = _renderParagraph.getFullHeightForCaret(position)!; + const cursorWidth = 2; + return Rect.fromLTWH( + cursorOffset.dx - (cursorWidth / 2), + cursorOffset.dy, + cursorWidth.toDouble(), + cursorHeight.toDouble(), + ); + } } extension on TextNode { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index fce1ac8ffd..9ce17e26de 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,4 +1,7 @@ +import 'dart:collection'; + import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/keyboard.dart'; import 'package:flowy_editor/operation/operation.dart'; import 'package:flowy_editor/render/selectable.dart'; import 'package:flutter/material.dart'; @@ -13,6 +16,7 @@ class EditorState { final StateTree document; final RenderPlugins renderPlugins; + Offset? tapOffset; Offset? panStartOffset; Offset? panEndOffset; @@ -25,11 +29,14 @@ class EditorState { /// TODO: move to a better place. Widget build(BuildContext context) { - return renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: document.root, - editorState: this, + return Keyboard( + editorState: this, + child: renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: document.root, + editorState: this, + ), ), ); } @@ -55,18 +62,45 @@ class EditorState { List selectionOverlays = []; + void updateCursor() { + if (tapOffset == null) { + return; + } + + // TODO: upward and backward + final selectedNode = _calculateSelectedNode(document.root, tapOffset!); + if (selectedNode.isEmpty) { + return; + } + final key = selectedNode.first.key; + if (key != null && key.currentState is Selectable) { + final selectable = key.currentState as Selectable; + final rect = selectable.getCursorRect(tapOffset!); + final overlay = OverlayEntry(builder: ((context) { + return Positioned.fromRect( + rect: rect, + child: Container( + color: Colors.red, + ), + ); + })); + selectionOverlays.add(overlay); + Overlay.of(selectable.context)?.insert(overlay); + } + } + void updateSelection() { selectionOverlays ..forEach((element) => element.remove()) ..clear(); - final selectedNodes = _selectedNodes; - if (selectedNodes.isEmpty) { + final selectedNodes = this.selectedNodes; + if (selectedNodes.isEmpty || + panStartOffset == null || + panEndOffset == null) { return; } - assert(panStartOffset != null && panEndOffset != null); - for (final node in selectedNodes) { final key = node.key; if (key != null && key.currentState is Selectable) { @@ -90,12 +124,46 @@ class EditorState { } } - List get _selectedNodes { - if (panStartOffset == null || panEndOffset == null) { - return []; + List get selectedNodes { + if (panStartOffset != null && panEndOffset != null) { + return _calculateSelectedNodes( + document.root, panStartOffset!, panEndOffset!); } - return _calculateSelectedNodes( - document.root, panStartOffset!, panEndOffset!); + if (tapOffset != null) { + return _calculateSelectedNode(document.root, tapOffset!); + } + return []; + } + + List _calculateSelectedNode(Node node, Offset offset) { + List result = []; + + /// Skip the node without parent because it is the topmost node. + /// Skip the node without key because it cannot get the [RenderObject]. + if (node.parent != null && node.key != null) { + if (_isNodeInOffset(node, offset)) { + result.add(node); + } + } + + /// + for (final child in node.children) { + result.addAll(_calculateSelectedNode(child, offset)); + } + + return result; + } + + bool _isNodeInOffset(Node node, Offset offset) { + assert(node.key != null); + final renderBox = + node.key?.currentContext?.findRenderObject() as RenderBox?; + if (renderBox == null) { + return false; + } + final boxOffset = renderBox.localToGlobal(Offset.zero); + final boxRect = boxOffset & renderBox.size; + return boxRect.contains(offset); } List _calculateSelectedNodes(Node node, Offset start, Offset end) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart b/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart new file mode 100644 index 0000000000..61de2eba90 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart @@ -0,0 +1,45 @@ +import 'package:flutter/services.dart'; + +import '../render/selectable.dart'; +import 'editor_state.dart'; +import 'package:flutter/material.dart'; + +class Keyboard extends StatelessWidget { + final Widget child; + final focusNode = FocusNode(); + final EditorState editorState; + + Keyboard({ + Key? key, + required this.child, + required this.editorState, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: focusNode, + autofocus: true, + onKey: _onKey, + child: child, + ); + } + + KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + if (event is! RawKeyDownEvent) { + return KeyEventResult.ignored; + } + List result = []; + for (final node in editorState.selectedNodes) { + if (node.key != null && + node.key?.currentState is KeyboardEventsRespondable) { + final respondable = node.key!.currentState as KeyboardEventsRespondable; + result.add(respondable.onKeyDown(event)); + } + } + if (result.contains(KeyEventResult.handled)) { + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart index 89991bf687..28942835a8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart @@ -5,4 +5,11 @@ mixin Selectable on State { /// Returns a [Rect] list for overlay. /// [start] and [end] are global offsets. List getOverlayRectsInRange(Offset start, Offset end); + + /// Returns a [Offset] for cursor + Rect getCursorRect(Offset start); +} + +mixin KeyboardEventsRespondable on State { + KeyEventResult onKeyDown(RawKeyEvent event); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection_overlay.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection_overlay.dart new file mode 100644 index 0000000000..e69de29bb2 From eb97141859594b789da08a2d5140659c9e30ff81 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 21 Jul 2022 20:04:25 +0800 Subject: [PATCH 049/121] feat: remove subtype render --- .../flowy_editor/example/assets/document.json | 143 +++++++++--------- .../lib/plugin/selected_text_node_widget.dart | 44 ++++-- .../example/lib/plugin/text_node_widget.dart | 10 +- .../flowy_editor/lib/document/node.dart | 6 +- .../flowy_editor/lib/editor_state.dart | 4 + .../lib/render/render_plugins.dart | 1 + 6 files changed, 116 insertions(+), 92 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index f74672345f..16635261db 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -1,71 +1,78 @@ { - "document": { - "type": "editor", - "attributes": {}, - "children": [ - { - "type": "text", - "delta": [ - { "insert": "👋 Welcome to AppFlowy!", "attributes": { "href": "https://www.appflowy.io/", "heading": "h1" } } - ], - "attributes": { - "subtype": "with-heading", - "heading": "h1" + "document": { + "type": "editor", + "attributes": {}, + "children": [ + { + "type": "text", + "delta": [ + { + "insert": "👋 Welcome to AppFlowy!", + "attributes": { + "href": "https://www.appflowy.io/", + "heading": "h1" + } } - }, - { - "type": "text", - "delta": [ - { "insert": "Here are the basics", "attributes": { "heading": "h2" } } - ], - "attributes": { - "subtype": "with-heading", - "heading": "h2" - } - }, - { - "type": "text", - "delta": [ - { "insert": "Click anywhere and just start typing." } - ], - "attributes": { - "subtype": "with-checkbox", - "checkbox": true - } - }, - { - "type": "text", - "delta": [ - { "insert": "Highlight", "attributes": { "highlight": "0xFFFFFF00" } }, - { "insert": " Click anywhere and just start typing" }, - { "insert": " any text, and use the menu at the bottom to " }, - { "insert": "style", "attributes": { "italic": true } }, - { "insert": " your ", "attributes": { "bold": true } }, - { "insert": "writing", "attributes": { "underline": true } }, - { "insert": " howeverv you like.", "attributes": { "strikethrough": true } } - ], - "attributes": { - "subtype": "with-checkbox", - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { "insert": "Have a question? ", "attributes": { "heading": "h2" } } - ], - "attributes": { - "subtype": "with-heading", - "heading": "h2" - } - }, - { - "type": "text", - "delta": [ - { "insert": "Click the '?' at the bottom right for help and support."} - ], - "attributes": {} + ], + "attributes": { + "heading": "h1" } - ] - } - } \ No newline at end of file + }, + { + "type": "text", + "delta": [ + { "insert": "Here are the basics", "attributes": { "heading": "h2" } } + ], + "attributes": { + "heading": "h2" + } + }, + { + "type": "text", + "delta": [{ "insert": "Click anywhere and just start typing." }], + "attributes": { + "checkbox": true + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Highlight", + "attributes": { "highlight": "0xFFFFFF00" } + }, + { "insert": " Click anywhere and just start typing" }, + { "insert": " any text, and use the menu at the bottom to " }, + { "insert": "style", "attributes": { "italic": true } }, + { "insert": " your ", "attributes": { "bold": true } }, + { "insert": "writing", "attributes": { "underline": true } }, + { + "insert": " howeverv you like.", + "attributes": { "strikethrough": true } + } + ], + "attributes": { + "checkbox": false + } + }, + { + "type": "text", + "delta": [ + { "insert": "Have a question? ", "attributes": { "heading": "h2" } } + ], + "attributes": { + "heading": "h2" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + } + ] + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 4d04bfb660..5eb8a1b40f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -20,6 +20,7 @@ class SelectedTextNodeBuilder extends NodeWidgetBuilder { @override Widget build(BuildContext buildContext) { + print('key -> $key'); return _SelectedTextNodeWidget( key: key, node: node, @@ -100,9 +101,20 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> // TODO: just handle upforward delete. if (textSelection != null) { if (textSelection.isCollapsed) { - TransactionBuilder(editorState) - ..deleteText(node, textSelection.start - 1, 1) - ..commit(); + print(node.toRawString()); + print('is empty ${node.toRawString().isEmpty}'); + if (textSelection.baseOffset == 0 && node.toRawString().isEmpty) { + TransactionBuilder(editorState) + ..deleteNode(node) + ..commit(); + } else { + TransactionBuilder(editorState) + ..deleteText(node, textSelection.start - 1, 1) + ..commit(); + final rect = _computeCursorRect(textSelection.baseOffset - 1); + editorState.tapOffset = rect.center; + editorState.updateCursor(); + } } else { TransactionBuilder(editorState) ..deleteText(node, textSelection.start, @@ -117,6 +129,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> @override Widget build(BuildContext context) { + print('text rebuild $this'); Widget richText; if (kDebugMode) { richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey); @@ -127,7 +140,10 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - richText, + SizedBox( + width: MediaQuery.of(context).size.width, + child: richText, + ), if (node.children.isNotEmpty) ...node.children.map( (e) => editorState.renderPlugins.buildWidget( @@ -163,14 +179,18 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> final position = TextPosition(offset: offset); var cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); cursorOffset = _renderParagraph.localToGlobal(cursorOffset); - final cursorHeight = _renderParagraph.getFullHeightForCaret(position)!; - const cursorWidth = 2; - return Rect.fromLTWH( - cursorOffset.dx - (cursorWidth / 2), - cursorOffset.dy, - cursorWidth.toDouble(), - cursorHeight.toDouble(), - ); + final cursorHeight = _renderParagraph.getFullHeightForCaret(position); + if (cursorHeight != null) { + const cursorWidth = 2; + return Rect.fromLTWH( + cursorOffset.dx - (cursorWidth / 2), + cursorOffset.dy, + cursorWidth.toDouble(), + cursorHeight.toDouble(), + ); + } else { + return Rect.zero; + } } } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 911a6179be..c265dc6254 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -103,7 +103,6 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> textCapitalization: TextCapitalization.sentences, ), ); - debugPrint('selection: $selection'); editorState.cursorSelection = _localSelectionToGlobal(node, selection); _textInputConnection ?..show() @@ -182,9 +181,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> } @override - void performAction(TextInputAction action) { - debugPrint('action:$action'); - } + void performAction(TextInputAction action) {} @override void performPrivateCommand(String action, Map data) { @@ -207,13 +204,10 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> } @override - void updateEditingValue(TextEditingValue value) { - debugPrint('offset: ${value.selection}'); - } + void updateEditingValue(TextEditingValue value) {} @override void updateEditingValueWithDeltas(List textEditingDeltas) { - debugPrint(textEditingDeltas.toString()); for (final textDelta in textEditingDeltas) { if (textDelta is TextEditingDeltaInsertion) { TransactionBuilder(editorState) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 8c75eca360..8010a40646 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -186,8 +186,6 @@ class TextNode extends Node { return map; } - String toRawString() => _delta.operations - .whereType() - .map((op) => op.content) - .toString(); + String toRawString() => + _delta.operations.whereType().map((op) => op.content).join(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 9ce17e26de..1d3f63a4ba 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -63,6 +63,10 @@ class EditorState { List selectionOverlays = []; void updateCursor() { + selectionOverlays + ..forEach((element) => element.remove()) + ..clear(); + if (tapOffset == null) { return; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart index efe5865d64..5d62c7b246 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -51,6 +51,7 @@ class RenderPlugins { _nodeWidgetBuilders.removeWhere((key, _) => key == name); } + @protected Widget buildWidget({ required NodeWidgetContext context, bool withSubtype = true, From a831ddc5895ac4c85c2cd120b8adf857b7ca4e49 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 22 Jul 2022 00:16:34 +0800 Subject: [PATCH 050/121] refactor: abstract selection and keyboard from editor state --- .../flowy_editor/example/assets/document.json | 117 ++++++++ .../flowy_editor/example/lib/main.dart | 4 +- .../lib/plugin/document_node_widget.dart | 71 +---- .../example/lib/plugin/image_node_widget.dart | 2 +- .../lib/plugin/selected_text_node_widget.dart | 2 +- .../flowy_editor/lib/editor_state.dart | 4 +- .../flowy_editor/lib/flowy_editor.dart | 1 + .../lib/flowy_editor_service.dart | 33 +++ .../lib/flowy_keyboard_service.dart | 70 +++++ .../lib/flowy_selection_service.dart | 279 ++++++++++++++++++ .../packages/flowy_editor/lib/keyboard.dart | 4 +- .../flowy_editor/lib/render/selectable.dart | 4 +- 12 files changed, 524 insertions(+), 67 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 16635261db..8356e31f7a 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -64,6 +64,123 @@ "heading": "h2" } }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, { "type": "text", "delta": [ diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 83bc2044e0..bdd5658e6b 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -94,7 +94,9 @@ class _MyHomePageState extends State { document: document, renderPlugins: renderPlugins, ); - return _editorState.build(context); + return FlowyEditor( + editorState: _editorState, + ); } }, ), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart index 4608f8b20c..f9ab3104da 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart @@ -33,66 +33,21 @@ class _EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return RawGestureDetector( - behavior: HitTestBehavior.translucent, - gestures: { - PanGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => PanGestureRecognizer(), - (recognizer) { - recognizer - ..onStart = _onPanStart - ..onUpdate = _onPanUpdate - ..onEnd = _onPanEnd; - }, - ), - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), - (recongizer) { - recongizer..onTapDown = _onTapDown; - }, - ) - }, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: e, - editorState: editorState, - ), + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (e) => editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: e, + editorState: editorState, ), - ) - .toList(), - ), + ), + ) + .toList(), ), ); } - - void _onTapDown(TapDownDetails details) { - editorState.panStartOffset = null; - editorState.panEndOffset = null; - editorState.updateSelection(); - - editorState.tapOffset = details.globalPosition; - editorState.updateCursor(); - } - - void _onPanStart(DragStartDetails details) { - editorState.panStartOffset = details.globalPosition; - editorState.updateSelection(); - } - - void _onPanUpdate(DragUpdateDetails details) { - editorState.panEndOffset = details.globalPosition; - editorState.updateSelection(); - } - - void _onPanEnd(DragEndDetails details) { - // do nothing - } } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 308477e294..d5e68bead2 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -40,7 +40,7 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> String get src => widget.node.attributes['image_src'] as String; @override - List getOverlayRectsInRange(Offset start, Offset end) { + List getSelectionRectsInSelection(Offset start, Offset end) { final renderBox = context.findRenderObject() as RenderBox; final size = renderBox.size; final boxOffset = renderBox.localToGlobal(Offset.zero); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 5eb8a1b40f..7c37d69c73 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -56,7 +56,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> _textKey.currentContext?.findRenderObject() as RenderParagraph; @override - List getOverlayRectsInRange(Offset start, Offset end) { + List getSelectionRectsInSelection(Offset start, Offset end) { var textSelection = TextSelection(baseOffset: 0, extentOffset: node.toRawString().length); // Returns select all if the start or end exceeds the size of the box diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 1d3f63a4ba..20c02a031a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -109,8 +109,8 @@ class EditorState { final key = node.key; if (key != null && key.currentState is Selectable) { final selectable = key.currentState as Selectable; - final overlayRects = - selectable.getOverlayRectsInRange(panStartOffset!, panEndOffset!); + final overlayRects = selectable.getSelectionRectsInSelection( + panStartOffset!, panEndOffset!); for (final rect in overlayRects) { // TODO: refactor overlay implement. final overlay = OverlayEntry(builder: ((context) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index f98e1b71b1..96fad4340b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -11,3 +11,4 @@ export 'package:flowy_editor/operation/transaction.dart'; export 'package:flowy_editor/operation/transaction_builder.dart'; export 'package:flowy_editor/operation/operation.dart'; export 'package:flowy_editor/editor_state.dart'; +export 'package:flowy_editor/flowy_editor_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart new file mode 100644 index 0000000000..78dd6809d3 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart @@ -0,0 +1,33 @@ +import 'package:flowy_editor/flowy_keyboard_service.dart'; +import 'package:flowy_editor/flowy_selection_service.dart'; + +import 'editor_state.dart'; +import 'package:flutter/material.dart'; + +class FlowyEditor extends StatefulWidget { + const FlowyEditor({ + Key? key, + required this.editorState, + }) : super(key: key); + + final EditorState editorState; + + @override + State createState() => _FlowyEditorState(); +} + +class _FlowyEditorState extends State { + EditorState get editorState => widget.editorState; + + @override + Widget build(BuildContext context) { + return FlowySelectionWidget( + editorState: editorState, + child: FlowyKeyboardWidget( + handlers: const [], + editorState: editorState, + child: editorState.build(context), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart new file mode 100644 index 0000000000..9593530033 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart @@ -0,0 +1,70 @@ +import 'package:flutter/services.dart'; + +import 'editor_state.dart'; +import 'package:flutter/material.dart'; + +abstract class FlowyKeyboardHandler { + final EditorState editorState; + final RawKeyEvent rawKeyEvent; + + FlowyKeyboardHandler({ + required this.editorState, + required this.rawKeyEvent, + }); + + KeyEventResult onKeyDown(); +} + +/// Process keyboard events +class FlowyKeyboardWidget extends StatefulWidget { + const FlowyKeyboardWidget({ + Key? key, + required this.handlers, + required this.editorState, + required this.child, + }) : super(key: key); + + final EditorState editorState; + final Widget child; + final List handlers; + + @override + State createState() => _FlowyKeyboardWidgetState(); +} + +class _FlowyKeyboardWidgetState extends State { + final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service'); + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: focusNode, + autofocus: true, + onKey: _onKey, + child: widget.child, + ); + } + + KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + if (event is! RawKeyDownEvent) { + return KeyEventResult.ignored; + } + + for (final handler in widget.handlers) { + debugPrint('handle keyboard event $event by $handler'); + + KeyEventResult result = handler.onKeyDown(); + + switch (result) { + case KeyEventResult.handled: + return KeyEventResult.handled; + case KeyEventResult.skipRemainingHandlers: + return KeyEventResult.skipRemainingHandlers; + case KeyEventResult.ignored: + break; + } + } + + return KeyEventResult.ignored; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart new file mode 100644 index 0000000000..77c8474a07 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart @@ -0,0 +1,279 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'editor_state.dart'; +import 'document/node.dart'; +import '../render/selectable.dart'; + +/// Process selection and cursor +mixin _FlowySelectionService on State { + /// [Pan] and [Tap] must be mutually exclusive. + /// Pan + Offset? panStartOffset; + Offset? panEndOffset; + + /// Tap + Offset? tapOffset; + + void updateSelection(); + + void updateCursor(); + + /// Returns selected node(s) + /// Returns empty list if no nodes are being selected. + List get selectedNodes; + + /// Compute selected node triggered by [Tap] + Node? computeSelectedNodeByTap( + Node node, + Offset offset, + ); + + /// Compute selected nodes triggered by [Pan] + List computeSelectedNodesByPan( + Node node, + Offset start, + Offset end, + ); + + /// Pan + bool isNodeInSelection( + Node node, + Offset start, + Offset end, + ); + + /// Tap + bool isNodeInOffset( + Node node, + Offset offset, + ); +} + +class FlowySelectionWidget extends StatefulWidget { + const FlowySelectionWidget({ + Key? key, + required this.editorState, + required this.child, + }) : super(key: key); + + final EditorState editorState; + final Widget child; + + @override + State createState() => _FlowySelectionWidgetState(); +} + +class _FlowySelectionWidgetState extends State + with _FlowySelectionService { + List selectionOverlays = []; + + EditorState get editorState => widget.editorState; + + @override + Widget build(BuildContext context) { + return RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: { + PanGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => PanGestureRecognizer(), + (recognizer) { + recognizer + ..onStart = _onPanStart + ..onUpdate = _onPanUpdate + ..onEnd = _onPanEnd; + }, + ), + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (recongizer) { + recongizer.onTapDown = _onTapDown; + }, + ) + }, + child: widget.child, + ); + } + + @override + void updateSelection() { + _clearOverlay(); + + final nodes = selectedNodes; + if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) { + assert(panStartOffset == null); + assert(panEndOffset == null); + return; + } + + for (final node in nodes) { + final selectable = node.key?.currentState as Selectable?; + if (selectable != null) { + final selectionRects = selectable.getSelectionRectsInSelection( + panStartOffset!, panEndOffset!); + for (final rect in selectionRects) { + final overlay = OverlayEntry( + builder: ((context) => Positioned.fromRect( + rect: rect, + child: Container( + color: Colors.yellow.withAlpha(100), + ), + )), + ); + selectionOverlays.add(overlay); + } + } + } + Overlay.of(context)?.insertAll(selectionOverlays); + } + + @override + void updateCursor() { + _clearOverlay(); + + if (tapOffset == null) { + assert(tapOffset == null); + return; + } + + final nodes = selectedNodes; + if (nodes.isEmpty) { + return; + } + + final selectedNode = nodes.first; + final selectable = selectedNode.key?.currentState as Selectable?; + if (selectable != null) { + final rect = selectable.getCursorRect(tapOffset!); + final cursor = OverlayEntry( + builder: ((context) => Positioned.fromRect( + rect: rect, + child: Container( + color: Colors.blue, + ), + )), + ); + selectionOverlays.add(cursor); + } + Overlay.of(context)?.insertAll(selectionOverlays); + } + + @override + List get selectedNodes { + if (panStartOffset != null && panEndOffset != null) { + return computeSelectedNodesByPan( + editorState.document.root, panStartOffset!, panEndOffset!); + } else if (tapOffset != null) { + final reuslt = + computeSelectedNodeByTap(editorState.document.root, tapOffset!); + if (reuslt != null) { + return [reuslt]; + } + } + return []; + } + + @override + Node? computeSelectedNodeByTap(Node node, Offset offset) { + assert(this.tapOffset != null); + final tapOffset = this.tapOffset; + if (tapOffset != null) {} + + if (node.parent != null && node.key != null) { + if (isNodeInOffset(node, offset)) { + return node; + } + } + + for (final child in node.children) { + final result = computeSelectedNodeByTap(child, offset); + if (result != null) { + return result; + } + } + + return null; + } + + @override + List computeSelectedNodesByPan(Node node, Offset start, Offset end) { + List result = []; + if (node.parent != null && node.key != null) { + if (isNodeInSelection(node, start, end)) { + result.add(node); + } + } + for (final child in node.children) { + result.addAll(computeSelectedNodesByPan(child, start, end)); + } + // TODO: sort the result + return result; + } + + @override + bool isNodeInOffset(Node node, Offset offset) { + assert(node.key != null); + final renderBox = + node.key?.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + final boxOffset = renderBox.localToGlobal(Offset.zero); + final boxRect = boxOffset & renderBox.size; + return boxRect.contains(offset); + } + return false; + } + + @override + bool isNodeInSelection(Node node, Offset start, Offset end) { + assert(node.key != null); + final renderBox = + node.key?.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + final rect = Rect.fromPoints(start, end); + final boxOffset = renderBox.localToGlobal(Offset.zero); + final boxRect = boxOffset & renderBox.size; + return rect.overlaps(boxRect); + } + return false; + } + + void _onTapDown(TapDownDetails details) { + debugPrint('on tap down'); + + // TODO: use setter to make them exclusive?? + tapOffset = details.globalPosition; + panStartOffset = null; + panEndOffset = null; + + updateCursor(); + } + + void _onPanStart(DragStartDetails details) { + debugPrint('on pan start'); + + panStartOffset = details.globalPosition; + panEndOffset = null; + tapOffset = null; + } + + void _onPanUpdate(DragUpdateDetails details) { + // debugPrint('on pan update'); + + panEndOffset = details.globalPosition; + tapOffset = null; + + updateSelection(); + } + + void _onPanEnd(DragEndDetails details) { + // do nothing + } + + void _clearOverlay() { + selectionOverlays + ..forEach((overlay) => overlay.remove()) + ..clear(); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart b/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart index 61de2eba90..0077b38130 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart @@ -1,7 +1,7 @@ -import 'package:flutter/services.dart'; - import '../render/selectable.dart'; import 'editor_state.dart'; + +import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; class Keyboard extends StatelessWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart index 28942835a8..f040eee98d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart @@ -4,9 +4,9 @@ import 'package:flutter/material.dart'; mixin Selectable on State { /// Returns a [Rect] list for overlay. /// [start] and [end] are global offsets. - List getOverlayRectsInRange(Offset start, Offset end); + List getSelectionRectsInSelection(Offset start, Offset end); - /// Returns a [Offset] for cursor + /// Returns a [Rect] for cursor Rect getCursorRect(Offset start); } From c643c02887d0b28fb634798c865717f550a2c5ac Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 22 Jul 2022 00:46:25 +0800 Subject: [PATCH 051/121] feat: add keyboard example --- .../lib/plugin/document_node_widget.dart | 1 - .../example/lib/plugin/image_node_widget.dart | 5 + .../lib/plugin/selected_text_node_widget.dart | 11 +- .../flowy_editor/lib/editor_state.dart | 149 +----------------- .../lib/flowy_editor_service.dart | 6 +- .../lib/flowy_keyboard_service.dart | 44 +++++- .../lib/flowy_selection_service.dart | 2 + .../packages/flowy_editor/lib/keyboard.dart | 28 ++-- .../flowy_editor/lib/render/selectable.dart | 5 +- 9 files changed, 79 insertions(+), 172 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart index f9ab3104da..2db1ef89c4 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart @@ -1,5 +1,4 @@ import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; class EditorNodeWidgetBuilder extends NodeWidgetBuilder { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index d5e68bead2..f1719db744 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -57,6 +57,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> return cursorOffset & size; } + @override + TextSelection? getTextSelection() { + return null; + } + @override KeyEventResult onKeyDown(RawKeyEvent event) { if (event.logicalKey == LogicalKeyboardKey.backspace) { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 7c37d69c73..356a21e4f2 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -94,6 +94,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _computeCursorRect(textSelection.baseOffset); } + @override + TextSelection? getTextSelection() { + return _textSelection; + } + @override KeyEventResult onKeyDown(RawKeyEvent event) { if (event.logicalKey == LogicalKeyboardKey.backspace) { @@ -111,9 +116,9 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> TransactionBuilder(editorState) ..deleteText(node, textSelection.start - 1, 1) ..commit(); - final rect = _computeCursorRect(textSelection.baseOffset - 1); - editorState.tapOffset = rect.center; - editorState.updateCursor(); + // final rect = _computeCursorRect(textSelection.baseOffset - 1); + // editorState.tapOffset = rect.center; + // editorState.updateCursor(); } } else { TransactionBuilder(editorState) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 20c02a031a..ea9b5bfee1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -15,10 +15,7 @@ import './render/render_plugins.dart'; class EditorState { final StateTree document; final RenderPlugins renderPlugins; - - Offset? tapOffset; - Offset? panStartOffset; - Offset? panEndOffset; + List selectedNodes = []; Selection? cursorSelection; @@ -59,148 +56,4 @@ class EditorState { document.textEdit(op.path, op.delta); } } - - List selectionOverlays = []; - - void updateCursor() { - selectionOverlays - ..forEach((element) => element.remove()) - ..clear(); - - if (tapOffset == null) { - return; - } - - // TODO: upward and backward - final selectedNode = _calculateSelectedNode(document.root, tapOffset!); - if (selectedNode.isEmpty) { - return; - } - final key = selectedNode.first.key; - if (key != null && key.currentState is Selectable) { - final selectable = key.currentState as Selectable; - final rect = selectable.getCursorRect(tapOffset!); - final overlay = OverlayEntry(builder: ((context) { - return Positioned.fromRect( - rect: rect, - child: Container( - color: Colors.red, - ), - ); - })); - selectionOverlays.add(overlay); - Overlay.of(selectable.context)?.insert(overlay); - } - } - - void updateSelection() { - selectionOverlays - ..forEach((element) => element.remove()) - ..clear(); - - final selectedNodes = this.selectedNodes; - if (selectedNodes.isEmpty || - panStartOffset == null || - panEndOffset == null) { - return; - } - - for (final node in selectedNodes) { - final key = node.key; - if (key != null && key.currentState is Selectable) { - final selectable = key.currentState as Selectable; - final overlayRects = selectable.getSelectionRectsInSelection( - panStartOffset!, panEndOffset!); - for (final rect in overlayRects) { - // TODO: refactor overlay implement. - final overlay = OverlayEntry(builder: ((context) { - return Positioned.fromRect( - rect: rect, - child: Container( - color: Colors.yellow.withAlpha(100), - ), - ); - })); - selectionOverlays.add(overlay); - Overlay.of(selectable.context)?.insert(overlay); - } - } - } - } - - List get selectedNodes { - if (panStartOffset != null && panEndOffset != null) { - return _calculateSelectedNodes( - document.root, panStartOffset!, panEndOffset!); - } - if (tapOffset != null) { - return _calculateSelectedNode(document.root, tapOffset!); - } - return []; - } - - List _calculateSelectedNode(Node node, Offset offset) { - List result = []; - - /// Skip the node without parent because it is the topmost node. - /// Skip the node without key because it cannot get the [RenderObject]. - if (node.parent != null && node.key != null) { - if (_isNodeInOffset(node, offset)) { - result.add(node); - } - } - - /// - for (final child in node.children) { - result.addAll(_calculateSelectedNode(child, offset)); - } - - return result; - } - - bool _isNodeInOffset(Node node, Offset offset) { - assert(node.key != null); - final renderBox = - node.key?.currentContext?.findRenderObject() as RenderBox?; - if (renderBox == null) { - return false; - } - final boxOffset = renderBox.localToGlobal(Offset.zero); - final boxRect = boxOffset & renderBox.size; - return boxRect.contains(offset); - } - - List _calculateSelectedNodes(Node node, Offset start, Offset end) { - List result = []; - - /// Skip the node without parent because it is the topmost node. - /// Skip the node without key because it cannot get the [RenderObject]. - if (node.parent != null && node.key != null) { - if (_isNodeInRange(node, start, end)) { - result.add(node); - } - } - - /// - for (final child in node.children) { - result.addAll(_calculateSelectedNodes(child, start, end)); - } - - return result; - } - - bool _isNodeInRange(Node node, Offset start, Offset end) { - assert(node.key != null); - final renderBox = - node.key?.currentContext?.findRenderObject() as RenderBox?; - - /// Return false directly if the [RenderBox] cannot found. - if (renderBox == null) { - return false; - } - - final rect = Rect.fromPoints(start, end); - final boxOffset = renderBox.localToGlobal(Offset.zero); - return rect.overlaps(boxOffset & renderBox.size); - } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart index 78dd6809d3..01b4fdf419 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart @@ -24,7 +24,11 @@ class _FlowyEditorState extends State { return FlowySelectionWidget( editorState: editorState, child: FlowyKeyboardWidget( - handlers: const [], + handlers: [ + FlowyKeyboradBackSpaceHandler( + editorState: editorState, + ) + ], editorState: editorState, child: editorState.build(context), ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart index 9593530033..c7752abae5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart @@ -1,3 +1,7 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/operation/transaction.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/selectable.dart'; import 'package:flutter/services.dart'; import 'editor_state.dart'; @@ -5,14 +9,44 @@ import 'package:flutter/material.dart'; abstract class FlowyKeyboardHandler { final EditorState editorState; - final RawKeyEvent rawKeyEvent; FlowyKeyboardHandler({ required this.editorState, - required this.rawKeyEvent, }); - KeyEventResult onKeyDown(); + KeyEventResult onKeyDown(RawKeyEvent event); +} + +class FlowyKeyboradBackSpaceHandler extends FlowyKeyboardHandler { + FlowyKeyboradBackSpaceHandler({ + required super.editorState, + }); + + @override + KeyEventResult onKeyDown(RawKeyEvent event) { + final selectedNodes = editorState.selectedNodes; + if (selectedNodes.isNotEmpty) { + // handle delete text + // TODO: type: cursor or selection + if (selectedNodes.length == 1) { + final node = selectedNodes.first; + if (node is TextNode) { + final selectable = node.key?.currentState as Selectable?; + final textSelection = selectable?.getTextSelection(); + if (textSelection != null) { + if (textSelection.isCollapsed) { + TransactionBuilder(editorState) + ..deleteText(node, textSelection.start - 1, 1) + ..commit(); + // TODO: update selection?? + } + } + } + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } } /// Process keyboard events @@ -46,6 +80,8 @@ class _FlowyKeyboardWidgetState extends State { } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + debugPrint('on keyboard event $event'); + if (event is! RawKeyDownEvent) { return KeyEventResult.ignored; } @@ -53,7 +89,7 @@ class _FlowyKeyboardWidgetState extends State { for (final handler in widget.handlers) { debugPrint('handle keyboard event $event by $handler'); - KeyEventResult result = handler.onKeyDown(); + KeyEventResult result = handler.onKeyDown(event); switch (result) { case KeyEventResult.handled: diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart index 77c8474a07..6c55d6f955 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart @@ -102,6 +102,7 @@ class _FlowySelectionWidgetState extends State _clearOverlay(); final nodes = selectedNodes; + editorState.selectedNodes = nodes; if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) { assert(panStartOffset == null); assert(panEndOffset == null); @@ -139,6 +140,7 @@ class _FlowySelectionWidgetState extends State } final nodes = selectedNodes; + editorState.selectedNodes = nodes; if (nodes.isEmpty) { return; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart b/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart index 0077b38130..4cb39ce31e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart @@ -26,20 +26,20 @@ class Keyboard extends StatelessWidget { } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { - if (event is! RawKeyDownEvent) { - return KeyEventResult.ignored; - } - List result = []; - for (final node in editorState.selectedNodes) { - if (node.key != null && - node.key?.currentState is KeyboardEventsRespondable) { - final respondable = node.key!.currentState as KeyboardEventsRespondable; - result.add(respondable.onKeyDown(event)); - } - } - if (result.contains(KeyEventResult.handled)) { - return KeyEventResult.handled; - } + // if (event is! RawKeyDownEvent) { + // return KeyEventResult.ignored; + // } + // List result = []; + // for (final node in editorState.selectedNodes) { + // if (node.key != null && + // node.key?.currentState is KeyboardEventsRespondable) { + // final respondable = node.key!.currentState as KeyboardEventsRespondable; + // result.add(respondable.onKeyDown(event)); + // } + // } + // if (result.contains(KeyEventResult.handled)) { + // return KeyEventResult.handled; + // } return KeyEventResult.ignored; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart index f040eee98d..8d1951996d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart @@ -6,8 +6,11 @@ mixin Selectable on State { /// [start] and [end] are global offsets. List getSelectionRectsInSelection(Offset start, Offset end); - /// Returns a [Rect] for cursor + /// Returns a [Rect] for cursor. Rect getCursorRect(Offset start); + + /// For [TextNode] only. + TextSelection? getTextSelection(); } mixin KeyboardEventsRespondable on State { From 34a1da8450573483a1f8e3ac7b416eb8479f368d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 22 Jul 2022 09:44:20 +0800 Subject: [PATCH 052/121] chore: delete unused code --- .../example/lib/plugin/image_node_widget.dart | 14 +----- .../lib/plugin/selected_text_node_widget.dart | 37 +-------------- .../flowy_editor/lib/editor_state.dart | 17 +++---- .../packages/flowy_editor/lib/keyboard.dart | 45 ------------------- .../lib/render/render_plugins.dart | 1 - .../flowy_editor/lib/render/selectable.dart | 4 -- .../lib/render/selection_overlay.dart | 0 7 files changed, 7 insertions(+), 111 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/selection_overlay.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index f1719db744..b235f8f481 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -33,8 +33,7 @@ class _ImageNodeWidget extends StatefulWidget { State<_ImageNodeWidget> createState() => __ImageNodeWidgetState(); } -class __ImageNodeWidgetState extends State<_ImageNodeWidget> - with Selectable, KeyboardEventsRespondable { +class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { Node get node => widget.node; EditorState get editorState => widget.editorState; String get src => widget.node.attributes['image_src'] as String; @@ -62,17 +61,6 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> return null; } - @override - KeyEventResult onKeyDown(RawKeyEvent event) { - if (event.logicalKey == LogicalKeyboardKey.backspace) { - TransactionBuilder(editorState) - ..deleteNode(node) - ..commit(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - @override Widget build(BuildContext context) { return _build(context); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 356a21e4f2..7ce7162b07 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SelectedTextNodeBuilder extends NodeWidgetBuilder { @@ -20,7 +19,6 @@ class SelectedTextNodeBuilder extends NodeWidgetBuilder { @override Widget build(BuildContext buildContext) { - print('key -> $key'); return _SelectedTextNodeWidget( key: key, node: node, @@ -45,7 +43,7 @@ class _SelectedTextNodeWidget extends StatefulWidget { } class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> - with Selectable, KeyboardEventsRespondable { + with Selectable { TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; @@ -99,39 +97,6 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _textSelection; } - @override - KeyEventResult onKeyDown(RawKeyEvent event) { - if (event.logicalKey == LogicalKeyboardKey.backspace) { - final textSelection = _textSelection; - // TODO: just handle upforward delete. - if (textSelection != null) { - if (textSelection.isCollapsed) { - print(node.toRawString()); - print('is empty ${node.toRawString().isEmpty}'); - if (textSelection.baseOffset == 0 && node.toRawString().isEmpty) { - TransactionBuilder(editorState) - ..deleteNode(node) - ..commit(); - } else { - TransactionBuilder(editorState) - ..deleteText(node, textSelection.start - 1, 1) - ..commit(); - // final rect = _computeCursorRect(textSelection.baseOffset - 1); - // editorState.tapOffset = rect.center; - // editorState.updateCursor(); - } - } else { - TransactionBuilder(editorState) - ..deleteText(node, textSelection.start, - textSelection.baseOffset - textSelection.extentOffset) - ..commit(); - } - } - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - @override Widget build(BuildContext context) { print('text rebuild $this'); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index ea9b5bfee1..ed917403b6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,9 +1,5 @@ -import 'dart:collection'; - import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/keyboard.dart'; import 'package:flowy_editor/operation/operation.dart'; -import 'package:flowy_editor/render/selectable.dart'; import 'package:flutter/material.dart'; import './document/state_tree.dart'; @@ -26,14 +22,11 @@ class EditorState { /// TODO: move to a better place. Widget build(BuildContext context) { - return Keyboard( - editorState: this, - child: renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: document.root, - editorState: this, - ), + return renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: document.root, + editorState: this, ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart b/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart deleted file mode 100644 index 4cb39ce31e..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart +++ /dev/null @@ -1,45 +0,0 @@ -import '../render/selectable.dart'; -import 'editor_state.dart'; - -import 'package:flutter/services.dart'; -import 'package:flutter/material.dart'; - -class Keyboard extends StatelessWidget { - final Widget child; - final focusNode = FocusNode(); - final EditorState editorState; - - Keyboard({ - Key? key, - required this.child, - required this.editorState, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Focus( - focusNode: focusNode, - autofocus: true, - onKey: _onKey, - child: child, - ); - } - - KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { - // if (event is! RawKeyDownEvent) { - // return KeyEventResult.ignored; - // } - // List result = []; - // for (final node in editorState.selectedNodes) { - // if (node.key != null && - // node.key?.currentState is KeyboardEventsRespondable) { - // final respondable = node.key!.currentState as KeyboardEventsRespondable; - // result.add(respondable.onKeyDown(event)); - // } - // } - // if (result.contains(KeyEventResult.handled)) { - // return KeyEventResult.handled; - // } - return KeyEventResult.ignored; - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart index 5d62c7b246..efe5865d64 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -51,7 +51,6 @@ class RenderPlugins { _nodeWidgetBuilders.removeWhere((key, _) => key == name); } - @protected Widget buildWidget({ required NodeWidgetContext context, bool withSubtype = true, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart index 8d1951996d..3631da106f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart @@ -12,7 +12,3 @@ mixin Selectable on State { /// For [TextNode] only. TextSelection? getTextSelection(); } - -mixin KeyboardEventsRespondable on State { - KeyEventResult onKeyDown(RawKeyEvent event); -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection_overlay.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection_overlay.dart deleted file mode 100644 index e69de29bb2..0000000000 From e16444f88e7ac956bdeee70e074e4febdd49509d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 22 Jul 2022 10:43:20 +0800 Subject: [PATCH 053/121] fix: make sure the state is implemnt selectable, otherwise return. --- .../lib/flowy_selection_service.dart | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart index 6c55d6f955..b2a9f2b9c8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart @@ -110,21 +110,22 @@ class _FlowySelectionWidgetState extends State } for (final node in nodes) { - final selectable = node.key?.currentState as Selectable?; - if (selectable != null) { - final selectionRects = selectable.getSelectionRectsInSelection( - panStartOffset!, panEndOffset!); - for (final rect in selectionRects) { - final overlay = OverlayEntry( - builder: ((context) => Positioned.fromRect( - rect: rect, - child: Container( - color: Colors.yellow.withAlpha(100), - ), - )), - ); - selectionOverlays.add(overlay); - } + if (node.key?.currentState is! Selectable) { + continue; + } + final selectable = node.key?.currentState as Selectable; + final selectionRects = selectable.getSelectionRectsInSelection( + panStartOffset!, panEndOffset!); + for (final rect in selectionRects) { + final overlay = OverlayEntry( + builder: ((context) => Positioned.fromRect( + rect: rect, + child: Container( + color: Colors.yellow.withAlpha(100), + ), + )), + ); + selectionOverlays.add(overlay); } } Overlay.of(context)?.insertAll(selectionOverlays); @@ -146,19 +147,20 @@ class _FlowySelectionWidgetState extends State } final selectedNode = nodes.first; - final selectable = selectedNode.key?.currentState as Selectable?; - if (selectable != null) { - final rect = selectable.getCursorRect(tapOffset!); - final cursor = OverlayEntry( - builder: ((context) => Positioned.fromRect( - rect: rect, - child: Container( - color: Colors.blue, - ), - )), - ); - selectionOverlays.add(cursor); + if (selectedNode.key?.currentState is! Selectable) { + return; } + final selectable = selectedNode.key?.currentState as Selectable; + final rect = selectable.getCursorRect(tapOffset!); + final cursor = OverlayEntry( + builder: ((context) => Positioned.fromRect( + rect: rect, + child: Container( + color: Colors.blue, + ), + )), + ); + selectionOverlays.add(cursor); Overlay.of(context)?.insertAll(selectionOverlays); } @@ -183,12 +185,6 @@ class _FlowySelectionWidgetState extends State final tapOffset = this.tapOffset; if (tapOffset != null) {} - if (node.parent != null && node.key != null) { - if (isNodeInOffset(node, offset)) { - return node; - } - } - for (final child in node.children) { final result = computeSelectedNodeByTap(child, offset); if (result != null) { @@ -196,6 +192,12 @@ class _FlowySelectionWidgetState extends State } } + if (node.parent != null && node.key != null) { + if (isNodeInOffset(node, offset)) { + return node; + } + } + return null; } From a6ede7dc7532246c3c99066792df45ddad89e0a6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 22 Jul 2022 15:45:57 +0800 Subject: [PATCH 054/121] feat: add a floating cursor and follow the document scroll. refactor the keyboard handler to a Function. --- .../flowy_editor/example/assets/document.json | 6 ++ .../flowy_editor/example/lib/main.dart | 3 + .../example/lib/plugin/image_node_widget.dart | 13 +++- .../flowy_editor/lib/document/node.dart | 3 + .../flowy_editor/lib/flowy_cursor_widget.dart | 60 +++++++++++++++++ .../flowy_editor/lib/flowy_editor.dart | 1 + .../lib/flowy_editor_service.dart | 7 +- .../lib/flowy_keyboard_service.dart | 66 +++++++------------ .../lib/flowy_selection_service.dart | 38 +++++------ .../lib/render/node_widget_builder.dart | 5 +- 10 files changed, 132 insertions(+), 70 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 8356e31f7a..e89a258206 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -3,6 +3,12 @@ "type": "editor", "attributes": {}, "children": [ + { + "type": "image", + "attributes": { + "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=1500w" + } + }, { "type": "text", "delta": [ diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index bdd5658e6b..1e047a23b4 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -96,6 +96,9 @@ class _MyHomePageState extends State { ); return FlowyEditor( editorState: _editorState, + keyEventHandler: [ + deleteSingleImageNode, + ], ); } }, diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index b235f8f481..934974ce8c 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,6 +1,17 @@ import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/flowy_keyboard_service.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; + +FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) { + final selectNodes = editorState.selectedNodes; + if (selectNodes.length != 1 || selectNodes.first.type != 'image') { + return KeyEventResult.ignored; + } + TransactionBuilder(editorState) + ..deleteNode(selectNodes.first) + ..commit(); + return KeyEventResult.handled; +}; class ImageNodeBuilder extends NodeWidgetBuilder { ImageNodeBuilder.create({ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 8010a40646..8b80fd0b51 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -11,6 +11,8 @@ class Node extends ChangeNotifier with LinkedListEntry { final Attributes attributes; GlobalKey? key; + // TODO: abstract a selectable node?? + final layerLink = LayerLink(); String? get subtype { // TODO: make 'subtype' as a const value. @@ -186,6 +188,7 @@ class TextNode extends Node { return map; } + // TODO: It's unneccesry to compute everytime. String toRawString() => _delta.operations.whereType().map((op) => op.content).join(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart new file mode 100644 index 0000000000..e9d3d62f54 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +class FlowyCursorWidget extends StatefulWidget { + const FlowyCursorWidget({ + Key? key, + required this.layerLink, + required this.rect, + required this.color, + this.blinkingInterval = 0.5, + }) : super(key: key); + + final double blinkingInterval; + final Color color; + final Rect rect; + final LayerLink layerLink; + + @override + State createState() => _FlowyCursorWidgetState(); +} + +class _FlowyCursorWidgetState extends State { + bool showCursor = true; + late Timer timer; + + @override + void initState() { + super.initState(); + + timer = Timer.periodic( + Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()), + (timer) { + setState(() { + showCursor = !showCursor; + }); + }); + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Positioned.fromRect( + rect: widget.rect, + child: CompositedTransformFollower( + link: widget.layerLink, + offset: Offset(widget.rect.center.dx, 0), + showWhenUnlinked: true, + child: Container( + color: showCursor ? widget.color : Colors.transparent, + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 96fad4340b..117c71c4ed 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -12,3 +12,4 @@ export 'package:flowy_editor/operation/transaction_builder.dart'; export 'package:flowy_editor/operation/operation.dart'; export 'package:flowy_editor/editor_state.dart'; export 'package:flowy_editor/flowy_editor_service.dart'; +export 'package:flowy_editor/flowy_keyboard_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart index 01b4fdf419..b10f1282cd 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart @@ -8,9 +8,11 @@ class FlowyEditor extends StatefulWidget { const FlowyEditor({ Key? key, required this.editorState, + required this.keyEventHandler, }) : super(key: key); final EditorState editorState; + final List keyEventHandler; @override State createState() => _FlowyEditorState(); @@ -25,9 +27,8 @@ class _FlowyEditorState extends State { editorState: editorState, child: FlowyKeyboardWidget( handlers: [ - FlowyKeyboradBackSpaceHandler( - editorState: editorState, - ) + flowyDeleteNodesHandler, + ...widget.keyEventHandler, ], editorState: editorState, child: editorState.build(context), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart index c7752abae5..65ab52dac9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart @@ -1,53 +1,31 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/operation/transaction.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/selectable.dart'; import 'package:flutter/services.dart'; import 'editor_state.dart'; import 'package:flutter/material.dart'; -abstract class FlowyKeyboardHandler { - final EditorState editorState; +typedef FlowyKeyEventHandler = KeyEventResult Function( + EditorState editorState, + RawKeyEvent event, +); - FlowyKeyboardHandler({ - required this.editorState, - }); - - KeyEventResult onKeyDown(RawKeyEvent event); -} - -class FlowyKeyboradBackSpaceHandler extends FlowyKeyboardHandler { - FlowyKeyboradBackSpaceHandler({ - required super.editorState, - }); - - @override - KeyEventResult onKeyDown(RawKeyEvent event) { - final selectedNodes = editorState.selectedNodes; - if (selectedNodes.isNotEmpty) { - // handle delete text - // TODO: type: cursor or selection - if (selectedNodes.length == 1) { - final node = selectedNodes.first; - if (node is TextNode) { - final selectable = node.key?.currentState as Selectable?; - final textSelection = selectable?.getTextSelection(); - if (textSelection != null) { - if (textSelection.isCollapsed) { - TransactionBuilder(editorState) - ..deleteText(node, textSelection.start - 1, 1) - ..commit(); - // TODO: update selection?? - } - } - } - } - return KeyEventResult.handled; - } +FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { + // Handle delete nodes. + final nodes = editorState.selectedNodes; + if (nodes.length <= 1) { return KeyEventResult.ignored; } -} + + debugPrint('delete nodes = $nodes'); + + nodes + .fold( + TransactionBuilder(editorState), + (previousValue, node) => previousValue..deleteNode(node), + ) + .commit(); + return KeyEventResult.handled; +}; /// Process keyboard events class FlowyKeyboardWidget extends StatefulWidget { @@ -60,7 +38,7 @@ class FlowyKeyboardWidget extends StatefulWidget { final EditorState editorState; final Widget child; - final List handlers; + final List handlers; @override State createState() => _FlowyKeyboardWidgetState(); @@ -89,7 +67,7 @@ class _FlowyKeyboardWidgetState extends State { for (final handler in widget.handlers) { debugPrint('handle keyboard event $event by $handler'); - KeyEventResult result = handler.onKeyDown(event); + KeyEventResult result = handler(widget.editorState, event); switch (result) { case KeyEventResult.handled: @@ -97,7 +75,7 @@ class _FlowyKeyboardWidgetState extends State { case KeyEventResult.skipRemainingHandlers: return KeyEventResult.skipRemainingHandlers; case KeyEventResult.ignored: - break; + continue; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart index b2a9f2b9c8..b460df9ec2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/flowy_cursor_widget.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -15,9 +16,9 @@ mixin _FlowySelectionService on State { /// Tap Offset? tapOffset; - void updateSelection(); + void updateSelection(Offset start, Offset end); - void updateCursor(); + void updateCursor(Offset offset); /// Returns selected node(s) /// Returns empty list if no nodes are being selected. @@ -66,6 +67,8 @@ class FlowySelectionWidget extends StatefulWidget { class _FlowySelectionWidgetState extends State with _FlowySelectionService { + final _cursorKey = GlobalKey(debugLabel: 'cursor'); + List selectionOverlays = []; EditorState get editorState => widget.editorState; @@ -98,14 +101,12 @@ class _FlowySelectionWidgetState extends State } @override - void updateSelection() { + void updateSelection(Offset start, Offset end) { _clearOverlay(); final nodes = selectedNodes; editorState.selectedNodes = nodes; - if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) { - assert(panStartOffset == null); - assert(panEndOffset == null); + if (nodes.isEmpty) { return; } @@ -114,8 +115,8 @@ class _FlowySelectionWidgetState extends State continue; } final selectable = node.key?.currentState as Selectable; - final selectionRects = selectable.getSelectionRectsInSelection( - panStartOffset!, panEndOffset!); + final selectionRects = + selectable.getSelectionRectsInSelection(start, end); for (final rect in selectionRects) { final overlay = OverlayEntry( builder: ((context) => Positioned.fromRect( @@ -132,14 +133,9 @@ class _FlowySelectionWidgetState extends State } @override - void updateCursor() { + void updateCursor(Offset offset) { _clearOverlay(); - if (tapOffset == null) { - assert(tapOffset == null); - return; - } - final nodes = selectedNodes; editorState.selectedNodes = nodes; if (nodes.isEmpty) { @@ -151,13 +147,13 @@ class _FlowySelectionWidgetState extends State return; } final selectable = selectedNode.key?.currentState as Selectable; - final rect = selectable.getCursorRect(tapOffset!); + final rect = selectable.getCursorRect(offset); final cursor = OverlayEntry( - builder: ((context) => Positioned.fromRect( + builder: ((context) => FlowyCursorWidget( + key: _cursorKey, rect: rect, - child: Container( - color: Colors.blue, - ), + color: Colors.red, + layerLink: selectedNode.layerLink, )), ); selectionOverlays.add(cursor); @@ -251,7 +247,7 @@ class _FlowySelectionWidgetState extends State panStartOffset = null; panEndOffset = null; - updateCursor(); + updateCursor(tapOffset!); } void _onPanStart(DragStartDetails details) { @@ -268,7 +264,7 @@ class _FlowySelectionWidgetState extends State panEndOffset = details.globalPosition; tapOffset = null; - updateSelection(); + updateSelection(panStartOffset!, panEndOffset!); } void _onPanEnd(DragEndDetails details) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index f349a0fe3d..a3d35f9dad 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -52,7 +52,10 @@ class NodeWidgetBuilder { builder: (_, __) => Consumer( builder: ((context, value, child) { debugPrint('Node changed, and rebuilding...'); - return build(context); + return CompositedTransformTarget( + link: node.layerLink, + child: build(context), + ); }), ), ); From a120853d0606ebe0cf71702d6e174248942c030f Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 21 Jul 2022 20:07:51 +0800 Subject: [PATCH 055/121] feat: redo --- .../example/lib/plugin/text_node_widget.dart | 15 ++++++++- .../flowy_editor/lib/editor_state.dart | 8 +++++ .../flowy_editor/lib/undo_manager.dart | 33 ++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index b81ffca0ab..9e193be761 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -43,6 +43,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; bool _metaKeyDown = false; + bool _shiftKeyDown = false; TextInputConnection? _textInputConnection; @@ -79,6 +80,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> } KeyEventResult _onKey(FocusNode focusNode, RawKeyEvent event) { + debugPrint('key: $event'); if (event is RawKeyDownEvent) { final sel = _globalSelectionToLocal(node, editorState.cursorSelection); if (event.logicalKey == LogicalKeyboardKey.backspace) { @@ -90,14 +92,25 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> } else if (event.logicalKey == LogicalKeyboardKey.metaLeft || event.logicalKey == LogicalKeyboardKey.metaRight) { _metaKeyDown = true; + } else if (event.logicalKey == LogicalKeyboardKey.shiftLeft || + event.logicalKey == LogicalKeyboardKey.shiftRight) { + _shiftKeyDown = true; } else if (event.logicalKey == LogicalKeyboardKey.keyZ && _metaKeyDown) { - editorState.undoManager.undo(); + if (_shiftKeyDown) { + editorState.undoManager.redo(); + } else { + editorState.undoManager.undo(); + } } } else if (event is RawKeyUpEvent) { if (event.logicalKey == LogicalKeyboardKey.metaLeft || event.logicalKey == LogicalKeyboardKey.metaRight) { _metaKeyDown = false; } + if (event.logicalKey == LogicalKeyboardKey.shiftLeft || + event.logicalKey == LogicalKeyboardKey.shiftRight) { + _shiftKeyDown = false; + } } return KeyEventResult.ignored; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 1e6e404385..ee0e625537 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -10,8 +10,10 @@ class ApplyOptions { /// whether the transaction should be recorded into /// the undo stack. final bool recordUndo; + final bool recordRedo; const ApplyOptions({ this.recordUndo = true, + this.recordRedo = false, }); } @@ -57,6 +59,12 @@ class EditorState { } undoItem.afterSelection = transaction.afterSelection; _debouncedSealHistoryItem(); + } else if (options.recordRedo) { + final redoItem = HistoryItem(); + redoItem.addAll(transaction.operations); + redoItem.beforeSelection = transaction.beforeSelection; + redoItem.afterSelection = transaction.afterSelection; + undoManager.redoStack.push(redoItem); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart index d01ffa351a..d47ec2359a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart @@ -5,6 +5,7 @@ import 'package:flowy_editor/operation/operation.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/operation/transaction.dart'; import 'package:flowy_editor/editor_state.dart'; +import 'package:flutter/foundation.dart'; /// This class contains operations committed by users. /// If a [HistoryItem] is not sealed, operations can be added sequentially. @@ -68,6 +69,10 @@ class FixedSizeStack { return last; } + clear() { + _list.clear(); + } + HistoryItem get last => _list.last; bool get isEmpty => _list.isEmpty; @@ -92,6 +97,7 @@ class UndoManager { } final last = undoStack.last; if (last.sealed) { + redoStack.clear(); final item = HistoryItem(); undoStack.push(item); return item; @@ -100,6 +106,7 @@ class UndoManager { } undo() { + debugPrint('undo'); final s = state; if (s == null) { return; @@ -109,6 +116,30 @@ class UndoManager { return; } final transaction = historyItem.toTransaction(s); - s.apply(transaction, const ApplyOptions(recordUndo: false)); + s.apply( + transaction, + const ApplyOptions( + recordUndo: false, + recordRedo: true, + )); + } + + redo() { + debugPrint('redo'); + final s = state; + if (s == null) { + return; + } + final historyItem = redoStack.pop(); + if (historyItem == null) { + return; + } + final transaction = historyItem.toTransaction(s); + s.apply( + transaction, + const ApplyOptions( + recordUndo: true, + recordRedo: false, + )); } } From 2661a6a4ae441f999fb7a9faac41f8992e6e18c4 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 24 Jul 2022 10:14:51 +0800 Subject: [PATCH 056/121] chore: update operation documentation --- frontend/.vscode/settings.json | 10 ++++------ .../flowy_editor/lib/operation/transaction.dart | 12 ++++-------- .../lib/operation/transaction_builder.dart | 14 ++++---------- .../packages/flowy_editor/lib/undo_manager.dart | 2 +- 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json index 2f5e152d62..d1732c5231 100644 --- a/frontend/.vscode/settings.json +++ b/frontend/.vscode/settings.json @@ -2,23 +2,21 @@ "[dart]": { "editor.formatOnSave": true, "editor.formatOnType": true, - "editor.rulers": [ - 120 - ], + "editor.rulers": [80], "editor.selectionHighlight": false, "editor.suggest.snippetsPreventQuickSuggestions": false, "editor.suggestSelection": "first", "editor.tabCompletion": "onlySnippets", - "editor.wordBasedSuggestions": false + "editor.wordBasedSuggestions": false, }, "svgviewer.enableautopreview": true, "svgviewer.previewcolumn": "Active", "svgviewer.showzoominout": true, - "editor.wordWrapColumn": 120, + "editor.wordWrapColumn": 80, "editor.minimap.maxColumn": 140, "prettier.printWidth": 140, "editor.wordWrap": "wordWrapColumn", - "dart.lineLength": 120, + "dart.lineLength": 80, "files.associations": { "*.log.*": "log" }, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart index 3de528e868..85bc43f537 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart @@ -3,19 +3,15 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/document/selection.dart'; import './operation.dart'; -/// This class to use to store the **changes** -/// will be applied to the editor. +/// A [Transaction] has a list of [Operation] objects that will be applied +/// to the editor. It is an immutable class and used to store and transmit. /// -/// This class is immutable version the the class -/// [[Transaction]]. Is used to stored and -/// transmit. If you want to build the transaction, -/// use [[Transaction]] directly. +/// If you want to build a new [Transaction], use [TransactionBuilder] directly. /// /// There will be several ways to consume the transaction: /// 1. Apply to the state to update the UI. /// 2. Send to the backend to store and do operation transforming. -/// 3. Stored by the UndoManager to implement redo/undo. -/// +/// 3. Used by the UndoManager to implement redo/undo. @immutable class Transaction { final UnmodifiableListView operations; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 6fca48f230..319002bd45 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -11,16 +11,10 @@ import 'package:flowy_editor/document/selection.dart'; import './operation.dart'; import './transaction.dart'; -/// -/// This class is used to -/// build the transaction from the state. -/// -/// This class automatically save the -/// cursor from the state. -/// -/// When the transaction is undo, the -/// cursor can be restored. -/// +/// A [TransactionBuilder] is used to build the transaction from the state. +/// It will save make a snapshot of the cursor selection state automatically. +/// The cursor can be resoted if the transaction is undo. + class TransactionBuilder { final List operations = []; EditorState state; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart index d47ec2359a..5b543f03a1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart @@ -7,7 +7,7 @@ import 'package:flowy_editor/operation/transaction.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flutter/foundation.dart'; -/// This class contains operations committed by users. +/// A [HistoryItem] contains list of operations committed by users. /// If a [HistoryItem] is not sealed, operations can be added sequentially. /// Otherwise, the operations should be added to a new [HistoryItem]. class HistoryItem extends LinkedListEntry { From f58a6c9523c917e240c3caa8b2594840deae863d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 25 Jul 2022 11:07:20 +0800 Subject: [PATCH 057/121] feat: implement floating cursor and selection --- .../flowy_editor/example/assets/document.json | 27 +++++++++ .../example/lib/plugin/image_node_widget.dart | 13 ++--- .../lib/plugin/selected_text_node_widget.dart | 32 +++++------ .../flowy_editor/lib/editor_state.dart | 12 ++-- .../flowy_editor/lib/flowy_editor.dart | 6 +- .../selection}/flowy_cursor_widget.dart | 2 +- .../selection/flowy_selection_widget.dart | 34 +++++++++++ .../render/{ => selection}/selectable.dart | 4 +- .../{ => service}/flowy_editor_service.dart | 8 +-- .../{ => service}/flowy_keyboard_service.dart | 2 +- .../flowy_selection_service.dart | 57 +++++++++++-------- 11 files changed, 133 insertions(+), 64 deletions(-) rename frontend/app_flowy/packages/flowy_editor/lib/{ => render/selection}/flowy_cursor_widget.dart (96%) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart rename frontend/app_flowy/packages/flowy_editor/lib/render/{ => selection}/selectable.dart (66%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => service}/flowy_editor_service.dart (79%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => service}/flowy_keyboard_service.dart (98%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => service}/flowy_selection_service.dart (82%) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index e89a258206..350764f769 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -97,6 +97,33 @@ ], "attributes": {} }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, { "type": "text", "delta": [ diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 934974ce8c..4b63e77f51 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,5 +1,4 @@ import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/flowy_keyboard_service.dart'; import 'package:flutter/material.dart'; FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) { @@ -50,20 +49,16 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { String get src => widget.node.attributes['image_src'] as String; @override - List getSelectionRectsInSelection(Offset start, Offset end) { + List getSelectionRectsInRange(Offset start, Offset end) { final renderBox = context.findRenderObject() as RenderBox; - final size = renderBox.size; - final boxOffset = renderBox.localToGlobal(Offset.zero); - return [boxOffset & size]; + return [Offset.zero & renderBox.size]; } @override Rect getCursorRect(Offset start) { final renderBox = context.findRenderObject() as RenderBox; - final size = Size(5, renderBox.size.height); - final boxOffset = renderBox.localToGlobal(Offset.zero); - final cursorOffset = - Offset(renderBox.size.width + boxOffset.dx, boxOffset.dy); + final size = Size(2, renderBox.size.height); + final cursorOffset = Offset(renderBox.size.width, 0); return cursorOffset & size; } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 7ce7162b07..3783eab4fa 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -54,7 +54,10 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> _textKey.currentContext?.findRenderObject() as RenderParagraph; @override - List getSelectionRectsInSelection(Offset start, Offset end) { + List getSelectionRectsInRange(Offset start, Offset end) { + final localStart = _renderParagraph.globalToLocal(start); + final localEnd = _renderParagraph.globalToLocal(end); + var textSelection = TextSelection(baseOffset: 0, extentOffset: node.toRawString().length); // Returns select all if the start or end exceeds the size of the box @@ -62,20 +65,20 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> var rects = _computeSelectionRects(textSelection); _textSelection = textSelection; - if (end.dy > start.dy) { + if (localEnd.dy > localStart.dy) { // downward - if (end.dy >= rects.last.bottom) { + if (localEnd.dy >= rects.last.bottom) { return rects; } } else { // upward - if (end.dy <= rects.first.top) { + if (localEnd.dy <= rects.first.top) { return rects; } } - final selectionBaseOffset = _getTextPositionAtOffset(start).offset; - final selectionExtentOffset = _getTextPositionAtOffset(end).offset; + final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; + final selectionExtentOffset = _getTextPositionAtOffset(localEnd).offset; textSelection = TextSelection( baseOffset: selectionBaseOffset, extentOffset: selectionExtentOffset, @@ -86,7 +89,8 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> @override Rect getCursorRect(Offset start) { - final selectionBaseOffset = _getTextPositionAtOffset(start).offset; + final localStart = _renderParagraph.globalToLocal(start); + final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; final textSelection = TextSelection.collapsed(offset: selectionBaseOffset); _textSelection = textSelection; return _computeCursorRect(textSelection.baseOffset); @@ -99,7 +103,6 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> @override Widget build(BuildContext context) { - print('text rebuild $this'); Widget richText; if (kDebugMode) { richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey); @@ -132,23 +135,18 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } TextPosition _getTextPositionAtOffset(Offset offset) { - final textOffset = _renderParagraph.globalToLocal(offset); - return _renderParagraph.getPositionForOffset(textOffset); + return _renderParagraph.getPositionForOffset(offset); } List _computeSelectionRects(TextSelection selection) { final textBoxes = _renderParagraph.getBoxesForSelection(selection); - return textBoxes - .map((box) => - _renderParagraph.localToGlobal(box.toRect().topLeft) & - box.toRect().size) - .toList(); + return textBoxes.map((box) => box.toRect()).toList(); } Rect _computeCursorRect(int offset) { final position = TextPosition(offset: offset); - var cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); - cursorOffset = _renderParagraph.localToGlobal(cursorOffset); + final cursorOffset = + _renderParagraph.getOffsetForCaret(position, Rect.zero); final cursorHeight = _renderParagraph.getFullHeightForCaret(position); if (cursorHeight != null) { const cursorWidth = 2; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index ced48242e2..f1fa65b33d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,11 +1,13 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/operation/operation.dart'; import 'dart:async'; -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/undo_manager.dart'; import 'package:flutter/material.dart'; -import './document/selection.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/document/state_tree.dart'; +import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/operation/transaction.dart'; +import 'package:flowy_editor/undo_manager.dart'; +import 'package:flowy_editor/render/render_plugins.dart'; class ApplyOptions { /// This flag indicates that diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 117c71c4ed..19c94ef327 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -6,10 +6,10 @@ export 'package:flowy_editor/document/path.dart'; export 'package:flowy_editor/document/text_delta.dart'; export 'package:flowy_editor/render/render_plugins.dart'; export 'package:flowy_editor/render/node_widget_builder.dart'; -export 'package:flowy_editor/render/selectable.dart'; +export 'package:flowy_editor/render/selection/selectable.dart'; export 'package:flowy_editor/operation/transaction.dart'; export 'package:flowy_editor/operation/transaction_builder.dart'; export 'package:flowy_editor/operation/operation.dart'; export 'package:flowy_editor/editor_state.dart'; -export 'package:flowy_editor/flowy_editor_service.dart'; -export 'package:flowy_editor/flowy_keyboard_service.dart'; +export 'package:flowy_editor/service/flowy_editor_service.dart'; +export 'package:flowy_editor/service/flowy_keyboard_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart similarity index 96% rename from frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart index e9d3d62f54..9ab61e5c47 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart @@ -49,7 +49,7 @@ class _FlowyCursorWidgetState extends State { rect: widget.rect, child: CompositedTransformFollower( link: widget.layerLink, - offset: Offset(widget.rect.center.dx, 0), + offset: widget.rect.topCenter, showWhenUnlinked: true, child: Container( color: showCursor ? widget.color : Colors.transparent, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart new file mode 100644 index 0000000000..f3def681e1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class FlowySelectionWidget extends StatefulWidget { + const FlowySelectionWidget({ + Key? key, + required this.layerLink, + required this.rect, + required this.color, + }) : super(key: key); + + final Color color; + final Rect rect; + final LayerLink layerLink; + + @override + State createState() => _FlowySelectionWidgetState(); +} + +class _FlowySelectionWidgetState extends State { + @override + Widget build(BuildContext context) { + return Positioned.fromRect( + rect: widget.rect, + child: CompositedTransformFollower( + link: widget.layerLink, + offset: widget.rect.topLeft, + showWhenUnlinked: true, + child: Container( + color: widget.color, + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart similarity index 66% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 3631da106f..1ba8f32b53 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -4,9 +4,11 @@ import 'package:flutter/material.dart'; mixin Selectable on State { /// Returns a [Rect] list for overlay. /// [start] and [end] are global offsets. - List getSelectionRectsInSelection(Offset start, Offset end); + /// The return result must be an local offset. + List getSelectionRectsInRange(Offset start, Offset end); /// Returns a [Rect] for cursor. + /// The return result must be an local offset. Rect getCursorRect(Offset start); /// For [TextNode] only. diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart similarity index 79% rename from frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart index b10f1282cd..0703e75022 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart @@ -1,7 +1,7 @@ -import 'package:flowy_editor/flowy_keyboard_service.dart'; -import 'package:flowy_editor/flowy_selection_service.dart'; +import 'package:flowy_editor/service/flowy_keyboard_service.dart'; +import 'package:flowy_editor/service/flowy_selection_service.dart'; -import 'editor_state.dart'; +import '../editor_state.dart'; import 'package:flutter/material.dart'; class FlowyEditor extends StatefulWidget { @@ -23,7 +23,7 @@ class _FlowyEditorState extends State { @override Widget build(BuildContext context) { - return FlowySelectionWidget( + return FlowySelectionService( editorState: editorState, child: FlowyKeyboardWidget( handlers: [ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart similarity index 98% rename from frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart index 65ab52dac9..68f295e0bd 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart @@ -1,7 +1,7 @@ import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flutter/services.dart'; -import 'editor_state.dart'; +import '../editor_state.dart'; import 'package:flutter/material.dart'; typedef FlowyKeyEventHandler = KeyEventResult Function( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart similarity index 82% rename from frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart index b460df9ec2..b75ea5703b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart @@ -1,10 +1,11 @@ -import 'package:flowy_editor/flowy_cursor_widget.dart'; +import 'package:flowy_editor/render/selection/flowy_cursor_widget.dart'; +import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'editor_state.dart'; -import 'document/node.dart'; -import '../render/selectable.dart'; +import '../editor_state.dart'; +import '../document/node.dart'; +import '../render/selection/selectable.dart'; /// Process selection and cursor mixin _FlowySelectionService on State { @@ -51,8 +52,8 @@ mixin _FlowySelectionService on State { ); } -class FlowySelectionWidget extends StatefulWidget { - const FlowySelectionWidget({ +class FlowySelectionService extends StatefulWidget { + const FlowySelectionService({ Key? key, required this.editorState, required this.child, @@ -62,14 +63,15 @@ class FlowySelectionWidget extends StatefulWidget { final Widget child; @override - State createState() => _FlowySelectionWidgetState(); + State createState() => _FlowySelectionServiceState(); } -class _FlowySelectionWidgetState extends State +class _FlowySelectionServiceState extends State with _FlowySelectionService { final _cursorKey = GlobalKey(debugLabel: 'cursor'); - List selectionOverlays = []; + final List _selectionOverlays = []; + final List _cursorOverlays = []; EditorState get editorState => widget.editorState; @@ -102,7 +104,7 @@ class _FlowySelectionWidgetState extends State @override void updateSelection(Offset start, Offset end) { - _clearOverlay(); + _clearAllOverlayEntries(); final nodes = selectedNodes; editorState.selectedNodes = nodes; @@ -115,26 +117,24 @@ class _FlowySelectionWidgetState extends State continue; } final selectable = node.key?.currentState as Selectable; - final selectionRects = - selectable.getSelectionRectsInSelection(start, end); + final selectionRects = selectable.getSelectionRectsInRange(start, end); for (final rect in selectionRects) { final overlay = OverlayEntry( - builder: ((context) => Positioned.fromRect( + builder: ((context) => FlowySelectionWidget( + color: Colors.yellow.withAlpha(100), + layerLink: node.layerLink, rect: rect, - child: Container( - color: Colors.yellow.withAlpha(100), - ), )), ); - selectionOverlays.add(overlay); + _selectionOverlays.add(overlay); } } - Overlay.of(context)?.insertAll(selectionOverlays); + Overlay.of(context)?.insertAll(_selectionOverlays); } @override void updateCursor(Offset offset) { - _clearOverlay(); + _clearAllOverlayEntries(); final nodes = selectedNodes; editorState.selectedNodes = nodes; @@ -156,8 +156,8 @@ class _FlowySelectionWidgetState extends State layerLink: selectedNode.layerLink, )), ); - selectionOverlays.add(cursor); - Overlay.of(context)?.insertAll(selectionOverlays); + _cursorOverlays.add(cursor); + Overlay.of(context)?.insertAll(_cursorOverlays); } @override @@ -271,8 +271,19 @@ class _FlowySelectionWidgetState extends State // do nothing } - void _clearOverlay() { - selectionOverlays + void _clearAllOverlayEntries() { + _clearSelection(); + _clearCursor(); + } + + void _clearSelection() { + _selectionOverlays + ..forEach((overlay) => overlay.remove()) + ..clear(); + } + + void _clearCursor() { + _cursorOverlays ..forEach((overlay) => overlay.remove()) ..clear(); } From e1d990e4ae2c92cf60fb36f5a5d6c65c60271c15 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 25 Jul 2022 14:14:40 +0800 Subject: [PATCH 058/121] feat: support floating selection and delete textnode --- .../flowy_editor/example/assets/document.json | 22 +++--- .../flowy_editor/example/lib/main.dart | 4 +- .../example/lib/plugin/image_node_widget.dart | 16 ++-- .../lib/plugin/selected_text_node_widget.dart | 8 ++ .../flowy_editor/lib/editor_state.dart | 3 + .../lib/extensions/object_extensions.dart | 8 ++ .../flowy_editor/lib/flowy_editor.dart | 3 +- .../lib/render/selection/selectable.dart | 3 + ...ditor_service.dart => editor_service.dart} | 12 ++- .../delete_nodes_handler.dart | 21 ++++++ .../delete_single_text_node_handler.dart | 73 +++++++++++++++++++ ...ard_service.dart => keyboard_service.dart} | 27 +------ ...on_service.dart => selection_service.dart} | 59 +++++++-------- 13 files changed, 176 insertions(+), 83 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_editor_service.dart => editor_service.dart} (64%) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_keyboard_service.dart => keyboard_service.dart} (65%) rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_selection_service.dart => selection_service.dart} (82%) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 350764f769..00ef06da5d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -74,7 +74,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "1. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -83,7 +83,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "2. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -92,7 +92,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "3. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -101,7 +101,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support." + "insert": "4. Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -110,7 +110,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "5. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -119,7 +119,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "6. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -128,7 +128,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "7. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -137,7 +137,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "8. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -146,7 +146,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "9. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -155,7 +155,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "10. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -164,7 +164,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "11. Click the '?' at the bottom right for help and support." } ], "attributes": {} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 1e047a23b4..83960275e6 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -96,9 +96,7 @@ class _MyHomePageState extends State { ); return FlowyEditor( editorState: _editorState, - keyEventHandler: [ - deleteSingleImageNode, - ], + keyEventHandler: const [], ); } }, diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 4b63e77f51..389bfed320 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,17 +1,6 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; -FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) { - final selectNodes = editorState.selectedNodes; - if (selectNodes.length != 1 || selectNodes.first.type != 'image') { - return KeyEventResult.ignored; - } - TransactionBuilder(editorState) - ..deleteNode(selectNodes.first) - ..commit(); - return KeyEventResult.handled; -}; - class ImageNodeBuilder extends NodeWidgetBuilder { ImageNodeBuilder.create({ required super.node, @@ -67,6 +56,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { return null; } + @override + Offset getOffsetByTextSelection(TextSelection textSelection) { + return Offset.zero; + } + @override Widget build(BuildContext context) { return _build(context); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 3783eab4fa..1124ec3cbb 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -93,6 +93,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; final textSelection = TextSelection.collapsed(offset: selectionBaseOffset); _textSelection = textSelection; + print('text selection = $textSelection'); return _computeCursorRect(textSelection.baseOffset); } @@ -101,6 +102,12 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _textSelection; } + @override + Offset getOffsetByTextSelection(TextSelection textSelection) { + final offset = _computeCursorRect(textSelection.baseOffset).center; + return _renderParagraph.localToGlobal(offset); + } + @override Widget build(BuildContext context) { Widget richText; @@ -148,6 +155,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> final cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); final cursorHeight = _renderParagraph.getFullHeightForCaret(position); + print('offset = $offset, cursorHeight = $cursorHeight'); if (cursorHeight != null) { const cursorWidth = 2; return Rect.fromLTWH( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index f1fa65b33d..04a5721ed9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -19,6 +19,9 @@ class ApplyOptions { }); } +// TODO +final selectionServiceKey = GlobalKey(); + class EditorState { final StateTree document; final RenderPlugins renderPlugins; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart new file mode 100644 index 0000000000..b1b6e53512 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart @@ -0,0 +1,8 @@ +extension FlowyObjectExtensions on Object { + T? unwrapOrNull() { + if (this is T) { + return this as T; + } + return null; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 19c94ef327..3f8510d8b3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -11,5 +11,4 @@ export 'package:flowy_editor/operation/transaction.dart'; export 'package:flowy_editor/operation/transaction_builder.dart'; export 'package:flowy_editor/operation/operation.dart'; export 'package:flowy_editor/editor_state.dart'; -export 'package:flowy_editor/service/flowy_editor_service.dart'; -export 'package:flowy_editor/service/flowy_keyboard_service.dart'; +export 'package:flowy_editor/service/editor_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 1ba8f32b53..59849c1a6a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -13,4 +13,7 @@ mixin Selectable on State { /// For [TextNode] only. TextSelection? getTextSelection(); + + /// For [TextNode] only. + Offset getOffsetByTextSelection(TextSelection textSelection); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart similarity index 64% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 0703e75022..d0efac2a0f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,5 +1,7 @@ -import 'package:flowy_editor/service/flowy_keyboard_service.dart'; -import 'package:flowy_editor/service/flowy_selection_service.dart'; +import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/service/selection_service.dart'; import '../editor_state.dart'; import 'package:flutter/material.dart'; @@ -23,11 +25,13 @@ class _FlowyEditorState extends State { @override Widget build(BuildContext context) { - return FlowySelectionService( + return FlowySelection( + key: selectionServiceKey, editorState: editorState, - child: FlowyKeyboardWidget( + child: FlowyKeyboard( handlers: [ flowyDeleteNodesHandler, + deleteSingleTextNodeHandler, ...widget.keyEventHandler, ], editorState: editorState, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart new file mode 100644 index 0000000000..dda52612e9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart @@ -0,0 +1,21 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; + +FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { + // Handle delete nodes. + final nodes = editorState.selectedNodes; + if (nodes.length <= 1) { + return KeyEventResult.ignored; + } + + debugPrint('delete nodes = $nodes'); + + nodes + .fold( + TransactionBuilder(editorState), + (previousValue, node) => previousValue..deleteNode(node), + ) + .commit(); + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart new file mode 100644 index 0000000000..3c1c1c9e95 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart @@ -0,0 +1,73 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flowy_editor/service/selection_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// TODO: need to be refactored, just a example code. +FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.backspace) { + return KeyEventResult.ignored; + } + + final selectionNodes = editorState.selectedNodes; + if (selectionNodes.length == 1 && selectionNodes.first is TextNode) { + final node = selectionNodes.first.unwrapOrNull(); + final selectable = node?.key?.currentState?.unwrapOrNull(); + if (selectable != null) { + final textSelection = selectable.getTextSelection(); + if (textSelection != null) { + if (textSelection.isCollapsed) { + /// Three cases: + /// Delete the zero character, + /// 1. if there is still text node in front of it, then merge them. + /// 2. if not, just ignore + /// Delete the non-zero character, + /// 3. delete the single character. + if (textSelection.baseOffset == 0) { + if (node?.previous != null && node?.previous is TextNode) { + final previous = node!.previous! as TextNode; + final newTextSelection = TextSelection.collapsed( + offset: previous.toRawString().length); + final selectionService = + selectionServiceKey.currentState as FlowySelectionService; + final previousSelectable = + previous.key?.currentState?.unwrapOrNull(); + final newOfset = previousSelectable + ?.getOffsetByTextSelection(newTextSelection); + if (newOfset != null) { + selectionService.updateCursor(newOfset); + } + // merge + TransactionBuilder(editorState) + ..deleteNode(node) + ..insertText( + previous, previous.toRawString().length, node.toRawString()) + ..commit(); + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + } else { + TransactionBuilder(editorState) + ..deleteText(node!, textSelection.baseOffset - 1, 1) + ..commit(); + final newTextSelection = + TextSelection.collapsed(offset: textSelection.baseOffset - 1); + final selectionService = + selectionServiceKey.currentState as FlowySelectionService; + final newOfset = + selectable.getOffsetByTextSelection(newTextSelection); + selectionService.updateCursor(newOfset); + return KeyEventResult.handled; + } + } + } + } + } + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart similarity index 65% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart index 68f295e0bd..060a9c98fb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart @@ -1,4 +1,3 @@ -import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flutter/services.dart'; import '../editor_state.dart'; @@ -9,27 +8,9 @@ typedef FlowyKeyEventHandler = KeyEventResult Function( RawKeyEvent event, ); -FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { - // Handle delete nodes. - final nodes = editorState.selectedNodes; - if (nodes.length <= 1) { - return KeyEventResult.ignored; - } - - debugPrint('delete nodes = $nodes'); - - nodes - .fold( - TransactionBuilder(editorState), - (previousValue, node) => previousValue..deleteNode(node), - ) - .commit(); - return KeyEventResult.handled; -}; - /// Process keyboard events -class FlowyKeyboardWidget extends StatefulWidget { - const FlowyKeyboardWidget({ +class FlowyKeyboard extends StatefulWidget { + const FlowyKeyboard({ Key? key, required this.handlers, required this.editorState, @@ -41,10 +22,10 @@ class FlowyKeyboardWidget extends StatefulWidget { final List handlers; @override - State createState() => _FlowyKeyboardWidgetState(); + State createState() => _FlowyKeyboardState(); } -class _FlowyKeyboardWidgetState extends State { +class _FlowyKeyboardState extends State { final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service'); @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart similarity index 82% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index b75ea5703b..99b0efb467 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -8,7 +8,7 @@ import '../document/node.dart'; import '../render/selection/selectable.dart'; /// Process selection and cursor -mixin _FlowySelectionService on State { +mixin FlowySelectionService on State { /// [Pan] and [Tap] must be mutually exclusive. /// Pan Offset? panStartOffset; @@ -19,20 +19,20 @@ mixin _FlowySelectionService on State { void updateSelection(Offset start, Offset end); - void updateCursor(Offset offset); + void updateCursor(Offset start); /// Returns selected node(s) /// Returns empty list if no nodes are being selected. - List get selectedNodes; + List getSelectedNodes(Offset start, [Offset? end]); /// Compute selected node triggered by [Tap] - Node? computeSelectedNodeByTap( + Node? computeSelectedNodeInOffset( Node node, Offset offset, ); /// Compute selected nodes triggered by [Pan] - List computeSelectedNodesByPan( + List computeSelectedNodesInRange( Node node, Offset start, Offset end, @@ -52,8 +52,8 @@ mixin _FlowySelectionService on State { ); } -class FlowySelectionService extends StatefulWidget { - const FlowySelectionService({ +class FlowySelection extends StatefulWidget { + const FlowySelection({ Key? key, required this.editorState, required this.child, @@ -63,11 +63,11 @@ class FlowySelectionService extends StatefulWidget { final Widget child; @override - State createState() => _FlowySelectionServiceState(); + State createState() => _FlowySelectionState(); } -class _FlowySelectionServiceState extends State - with _FlowySelectionService { +class _FlowySelectionState extends State + with FlowySelectionService { final _cursorKey = GlobalKey(debugLabel: 'cursor'); final List _selectionOverlays = []; @@ -106,7 +106,7 @@ class _FlowySelectionServiceState extends State void updateSelection(Offset start, Offset end) { _clearAllOverlayEntries(); - final nodes = selectedNodes; + final nodes = getSelectedNodes(start, end); editorState.selectedNodes = nodes; if (nodes.isEmpty) { return; @@ -133,10 +133,10 @@ class _FlowySelectionServiceState extends State } @override - void updateCursor(Offset offset) { + void updateCursor(Offset start) { _clearAllOverlayEntries(); - final nodes = selectedNodes; + final nodes = getSelectedNodes(start); editorState.selectedNodes = nodes; if (nodes.isEmpty) { return; @@ -147,7 +147,7 @@ class _FlowySelectionServiceState extends State return; } final selectable = selectedNode.key?.currentState as Selectable; - final rect = selectable.getCursorRect(offset); + final rect = selectable.getCursorRect(start); final cursor = OverlayEntry( builder: ((context) => FlowyCursorWidget( key: _cursorKey, @@ -161,13 +161,18 @@ class _FlowySelectionServiceState extends State } @override - List get selectedNodes { - if (panStartOffset != null && panEndOffset != null) { - return computeSelectedNodesByPan( - editorState.document.root, panStartOffset!, panEndOffset!); - } else if (tapOffset != null) { - final reuslt = - computeSelectedNodeByTap(editorState.document.root, tapOffset!); + List getSelectedNodes(Offset start, [Offset? end]) { + if (end != null) { + return computeSelectedNodesInRange( + editorState.document.root, + start, + end, + ); + } else { + final reuslt = computeSelectedNodeInOffset( + editorState.document.root, + start, + ); if (reuslt != null) { return [reuslt]; } @@ -176,13 +181,9 @@ class _FlowySelectionServiceState extends State } @override - Node? computeSelectedNodeByTap(Node node, Offset offset) { - assert(this.tapOffset != null); - final tapOffset = this.tapOffset; - if (tapOffset != null) {} - + Node? computeSelectedNodeInOffset(Node node, Offset offset) { for (final child in node.children) { - final result = computeSelectedNodeByTap(child, offset); + final result = computeSelectedNodeInOffset(child, offset); if (result != null) { return result; } @@ -198,7 +199,7 @@ class _FlowySelectionServiceState extends State } @override - List computeSelectedNodesByPan(Node node, Offset start, Offset end) { + List computeSelectedNodesInRange(Node node, Offset start, Offset end) { List result = []; if (node.parent != null && node.key != null) { if (isNodeInSelection(node, start, end)) { @@ -206,7 +207,7 @@ class _FlowySelectionServiceState extends State } } for (final child in node.children) { - result.addAll(computeSelectedNodesByPan(child, start, end)); + result.addAll(computeSelectedNodesInRange(child, start, end)); } // TODO: sort the result return result; From 2f86cac8af9343b3812bfca42f6edfbe003c20a7 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 25 Jul 2022 15:58:57 +0800 Subject: [PATCH 059/121] feat: Implement arrow up/down/left/right event handler. #708 --- .../example/lib/plugin/image_node_widget.dart | 10 +++++ .../lib/plugin/selected_text_node_widget.dart | 30 +++++++++++++++ .../flowy_editor/lib/editor_state.dart | 8 ++-- .../lib/render/selection/selectable.dart | 6 +++ .../lib/service/editor_service.dart | 5 ++- .../arrow_keys_handler.dart | 37 +++++++++++++++++++ .../delete_single_text_node_handler.dart | 6 +-- .../lib/service/keyboard_service.dart | 2 +- .../flowy_editor/lib/service/service.dart | 15 ++++++++ 9 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/service.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 389bfed320..193cc879c9 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -61,6 +61,16 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { return Offset.zero; } + @override + Offset getLeftOfOffset() { + return Offset.zero; + } + + @override + Offset getRightOfOffset() { + return Offset.zero; + } + @override Widget build(BuildContext context) { return _build(context); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 1124ec3cbb..d827fdfde8 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:example/plugin/debuggable_rich_text.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/foundation.dart'; @@ -108,6 +110,30 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _renderParagraph.localToGlobal(offset); } + @override + Offset getLeftOfOffset() { + final textSelection = _textSelection; + if (textSelection != null) { + final leftTextSelection = TextSelection.collapsed( + offset: max(0, textSelection.baseOffset - 1), + ); + return getOffsetByTextSelection(leftTextSelection); + } + return Offset.zero; + } + + @override + Offset getRightOfOffset() { + final textSelection = _textSelection; + if (textSelection != null) { + final leftTextSelection = TextSelection.collapsed( + offset: min(node.toRawString().length, textSelection.extentOffset + 1), + ); + return getOffsetByTextSelection(leftTextSelection); + } + return Offset.zero; + } + @override Widget build(BuildContext context) { Widget richText; @@ -117,6 +143,10 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> richText = RichText(key: _textKey, text: node.toTextSpan()); } + if (node.children.isEmpty) { + return richText; + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 04a5721ed9..000badf02a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:flowy_editor/service/service.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/document/node.dart'; @@ -19,13 +20,14 @@ class ApplyOptions { }); } -// TODO -final selectionServiceKey = GlobalKey(); - class EditorState { final StateTree document; final RenderPlugins renderPlugins; List selectedNodes = []; + + // Service reference. + final service = FlowyService(); + final UndoManager undoManager = UndoManager(); Selection? cursorSelection; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 59849c1a6a..098c246569 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -11,6 +11,12 @@ mixin Selectable on State { /// The return result must be an local offset. Rect getCursorRect(Offset start); + /// Returns one unit offset to the left of the offset + Offset getLeftOfOffset(/* Cause */); + + /// Returns one unit offset to the right of the offset + Offset getRightOfOffset(/* Cause */); + /// For [TextNode] only. TextSelection? getTextSelection(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index d0efac2a0f..2ebb9ce14a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/service/flowy_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart'; import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; @@ -26,12 +27,14 @@ class _FlowyEditorState extends State { @override Widget build(BuildContext context) { return FlowySelection( - key: selectionServiceKey, + key: editorState.service.selectionServiceKey, editorState: editorState, child: FlowyKeyboard( + key: editorState.service.keyboardServiceKey, handlers: [ flowyDeleteNodesHandler, deleteSingleTextNodeHandler, + arrowKeysHandler, ...widget.keyEventHandler, ], editorState: editorState, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart new file mode 100644 index 0000000000..4de5b61968 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart @@ -0,0 +1,37 @@ +import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.arrowUp && + event.logicalKey != LogicalKeyboardKey.arrowDown && + event.logicalKey != LogicalKeyboardKey.arrowLeft && + event.logicalKey != LogicalKeyboardKey.arrowRight) { + return KeyEventResult.ignored; + } + + // TODO: Up and Down + + // Left and Right + final selectedNodes = editorState.selectedNodes; + if (selectedNodes.length != 1) { + return KeyEventResult.ignored; + } + + final node = selectedNodes.first.unwrapOrNull(); + final selectable = node?.key?.currentState?.unwrapOrNull(); + Offset? offset; + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + offset = selectable?.getLeftOfOffset(); + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + offset = selectable?.getRightOfOffset(); + } + final selectionService = editorState.service.selectionService; + if (offset != null) { + selectionService.updateCursor(offset); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart index 3c1c1c9e95..5affb8800f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart @@ -33,8 +33,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { final previous = node!.previous! as TextNode; final newTextSelection = TextSelection.collapsed( offset: previous.toRawString().length); - final selectionService = - selectionServiceKey.currentState as FlowySelectionService; + final selectionService = editorState.service.selectionService; final previousSelectable = previous.key?.currentState?.unwrapOrNull(); final newOfset = previousSelectable @@ -58,8 +57,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { ..commit(); final newTextSelection = TextSelection.collapsed(offset: textSelection.baseOffset - 1); - final selectionService = - selectionServiceKey.currentState as FlowySelectionService; + final selectionService = editorState.service.selectionService; final newOfset = selectable.getOffsetByTextSelection(newTextSelection); selectionService.updateCursor(newOfset); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart index 060a9c98fb..ebd66894a7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart @@ -46,7 +46,7 @@ class _FlowyKeyboardState extends State { } for (final handler in widget.handlers) { - debugPrint('handle keyboard event $event by $handler'); + // debugPrint('handle keyboard event $event by $handler'); KeyEventResult result = handler(widget.editorState, event); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart new file mode 100644 index 0000000000..8c436cee7f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -0,0 +1,15 @@ +import 'package:flowy_editor/service/selection_service.dart'; +import 'package:flutter/material.dart'; + +class FlowyService { + // selection service + final selectionServiceKey = GlobalKey(debugLabel: 'flowy_selection_service'); + FlowySelectionService get selectionService { + assert(selectionServiceKey.currentState != null && + selectionServiceKey.currentState is FlowySelectionService); + return selectionServiceKey.currentState! as FlowySelectionService; + } + + // keyboard service + final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service'); +} From 0bf1e61d5503be09463e5cab7fed892b858e8e0d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 25 Jul 2022 18:28:00 +0800 Subject: [PATCH 060/121] feat: implement floating shortcut --- .../flowy_editor/example/lib/main.dart | 15 +++++ ..._cursor_widget.dart => cursor_widget.dart} | 8 +-- .../selection/floating_shortcut_widget.dart | 58 +++++++++++++++++++ ...tion_widget.dart => selection_widget.dart} | 8 +-- .../lib/service/editor_service.dart | 14 ++++- .../service/floating_shortcut_service.dart | 58 +++++++++++++++++++ .../delete_single_text_node_handler.dart | 2 - .../shortcut_handler.dart | 30 ++++++++++ .../lib/service/selection_service.dart | 18 ++++-- .../flowy_editor/lib/service/service.dart | 12 ++++ 10 files changed, 208 insertions(+), 15 deletions(-) rename frontend/app_flowy/packages/flowy_editor/lib/render/selection/{flowy_cursor_widget.dart => cursor_widget.dart} (83%) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart rename frontend/app_flowy/packages/flowy_editor/lib/render/selection/{flowy_selection_widget.dart => selection_widget.dart} (71%) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 83960275e6..6764c2faf1 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -97,6 +97,21 @@ class _MyHomePageState extends State { return FlowyEditor( editorState: _editorState, keyEventHandler: const [], + shortCuts: [ + // TODO: this won't work, just a example for now. + { + 'heading': (editorState, eventName) => + debugPrint('shortcut => $eventName') + }, + { + 'bold': (editorState, eventName) => + debugPrint('shortcut => $eventName') + }, + { + 'underline': (editorState, eventName) => + debugPrint('shortcut => $eventName') + }, + ], ); } }, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart similarity index 83% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart index 9ab61e5c47..2ba42221f0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -class FlowyCursorWidget extends StatefulWidget { - const FlowyCursorWidget({ +class CursorWidget extends StatefulWidget { + const CursorWidget({ Key? key, required this.layerLink, required this.rect, @@ -17,10 +17,10 @@ class FlowyCursorWidget extends StatefulWidget { final LayerLink layerLink; @override - State createState() => _FlowyCursorWidgetState(); + State createState() => _CursorWidgetState(); } -class _FlowyCursorWidgetState extends State { +class _CursorWidgetState extends State { bool showCursor = true; late Timer timer; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart new file mode 100644 index 0000000000..b91ed19fe2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart @@ -0,0 +1,58 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; + +typedef FloatingShortCutHandler = void Function( + EditorState editorState, String eventName); +typedef FloatingShortCuts = List>; + +class FloatingShortcutWidget extends StatelessWidget { + const FloatingShortcutWidget({ + Key? key, + required this.editorState, + required this.layerLink, + required this.rect, + required this.floatingShortcuts, + }) : super(key: key); + + final EditorState editorState; + final LayerLink layerLink; + final Rect rect; + final FloatingShortCuts floatingShortcuts; + + List get _shortcutNames => + floatingShortcuts.map((shortcut) => shortcut.keys.first).toList(); + List get _shortcutHandlers => + floatingShortcuts.map((shortcut) => shortcut.values.first).toList(); + + @override + Widget build(BuildContext context) { + return Positioned.fromRect( + rect: rect, + child: CompositedTransformFollower( + link: layerLink, + offset: rect.topLeft, + showWhenUnlinked: true, + child: Container( + color: Colors.white, + child: ListView.builder( + itemCount: floatingShortcuts.length, + itemBuilder: ((context, index) { + final name = _shortcutNameInIndex(index); + final handler = _shortCutHandlerInIndex(index); + return Card( + child: GestureDetector( + onTap: () => handler(editorState, name), + child: ListTile(title: Text(name)), + ), + ); + }), + ), + ), + ), + ); + } + + String _shortcutNameInIndex(int index) => _shortcutNames[index]; + FloatingShortCutHandler _shortCutHandlerInIndex(int index) => + _shortcutHandlers[index]; +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart similarity index 71% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart index f3def681e1..96dd6a7759 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class FlowySelectionWidget extends StatefulWidget { - const FlowySelectionWidget({ +class SelectionWidget extends StatefulWidget { + const SelectionWidget({ Key? key, required this.layerLink, required this.rect, @@ -13,10 +13,10 @@ class FlowySelectionWidget extends StatefulWidget { final LayerLink layerLink; @override - State createState() => _FlowySelectionWidgetState(); + State createState() => _SelectionWidgetState(); } -class _FlowySelectionWidgetState extends State { +class _SelectionWidgetState extends State { @override Widget build(BuildContext context) { return Positioned.fromRect( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 2ebb9ce14a..0571cf4e03 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,6 +1,9 @@ +import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; +import 'package:flowy_editor/service/floating_shortcut_service.dart'; import 'package:flowy_editor/service/flowy_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart'; import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart'; +import 'package:flowy_editor/service/flowy_key_event_handlers/shortcut_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; @@ -12,10 +15,12 @@ class FlowyEditor extends StatefulWidget { Key? key, required this.editorState, required this.keyEventHandler, + required this.shortCuts, }) : super(key: key); final EditorState editorState; final List keyEventHandler; + final FloatingShortCuts shortCuts; @override State createState() => _FlowyEditorState(); @@ -32,13 +37,20 @@ class _FlowyEditorState extends State { child: FlowyKeyboard( key: editorState.service.keyboardServiceKey, handlers: [ + slashShortcutHandler, flowyDeleteNodesHandler, deleteSingleTextNodeHandler, arrowKeysHandler, ...widget.keyEventHandler, ], editorState: editorState, - child: editorState.build(context), + child: FloatingShortCut( + key: editorState.service.floatingShortcutServiceKey, + size: const Size(200, 150), // TODO: support customize size. + editorState: editorState, + floatingShortCuts: widget.shortCuts, + child: editorState.build(context), + ), ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart new file mode 100644 index 0000000000..ed1a6a4528 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart @@ -0,0 +1,58 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; +import 'package:flutter/material.dart'; + +mixin FlowyFloatingShortCutService { + void showInOffset(Offset offset, LayerLink layerLink); + void hide(); +} + +class FloatingShortCut extends StatefulWidget { + const FloatingShortCut({ + Key? key, + required this.size, + required this.editorState, + required this.floatingShortCuts, + required this.child, + }) : super(key: key); + + final Size size; + final EditorState editorState; + final Widget child; + final FloatingShortCuts floatingShortCuts; + + @override + State createState() => _FloatingShortCutState(); +} + +class _FloatingShortCutState extends State + with FlowyFloatingShortCutService { + OverlayEntry? _floatintShortcutOverlay; + + @override + void showInOffset(Offset offset, LayerLink layerLink) { + _floatintShortcutOverlay?.remove(); + _floatintShortcutOverlay = OverlayEntry( + builder: (context) => FloatingShortcutWidget( + editorState: widget.editorState, + layerLink: layerLink, + rect: offset.translate(10, 0) & widget.size, + floatingShortcuts: widget.floatingShortCuts), + ); + Overlay.of(context)?.insert(_floatintShortcutOverlay!); + } + + @override + void hide() { + _floatintShortcutOverlay?.remove(); + _floatintShortcutOverlay = null; + } + + @override + Widget build(BuildContext context) { + return Container( + child: widget.child, + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart index 5affb8800f..1358276f47 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart @@ -1,10 +1,8 @@ import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; -import 'package:flowy_editor/service/selection_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart new file mode 100644 index 0000000000..074e021f79 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart @@ -0,0 +1,30 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// type '/' to trigger shortcut widget +FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.slash) { + return KeyEventResult.ignored; + } + + final selectedNodes = editorState.selectedNodes; + if (selectedNodes.length != 1) { + return KeyEventResult.ignored; + } + + final textNode = selectedNodes.first.unwrapOrNull(); + final selectable = textNode?.key?.currentState?.unwrapOrNull(); + final textSelection = selectable?.getTextSelection(); + if (textNode != null && selectable != null && textSelection != null) { + final offset = selectable.getOffsetByTextSelection(textSelection); + final rect = selectable.getCursorRect(offset); + editorState.service.floatingToolbarService + .showInOffset(rect.topLeft, textNode.layerLink); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 99b0efb467..778e657340 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,5 +1,7 @@ -import 'package:flowy_editor/render/selection/flowy_cursor_widget.dart'; -import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; +import 'package:flowy_editor/render/selection/cursor_widget.dart'; +import 'package:flowy_editor/render/selection/selection_widget.dart'; +import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flowy_editor/service/floating_shortcut_service.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -120,7 +122,7 @@ class _FlowySelectionState extends State final selectionRects = selectable.getSelectionRectsInRange(start, end); for (final rect in selectionRects) { final overlay = OverlayEntry( - builder: ((context) => FlowySelectionWidget( + builder: ((context) => SelectionWidget( color: Colors.yellow.withAlpha(100), layerLink: node.layerLink, rect: rect, @@ -149,7 +151,7 @@ class _FlowySelectionState extends State final selectable = selectedNode.key?.currentState as Selectable; final rect = selectable.getCursorRect(start); final cursor = OverlayEntry( - builder: ((context) => FlowyCursorWidget( + builder: ((context) => CursorWidget( key: _cursorKey, rect: rect, color: Colors.red, @@ -275,6 +277,7 @@ class _FlowySelectionState extends State void _clearAllOverlayEntries() { _clearSelection(); _clearCursor(); + _clearFloatingShorts(); } void _clearSelection() { @@ -288,4 +291,11 @@ class _FlowySelectionState extends State ..forEach((overlay) => overlay.remove()) ..clear(); } + + void _clearFloatingShorts() { + final shortCutService = editorState + .service.floatingShortcutServiceKey.currentState + ?.unwrapOrNull(); + shortCutService?.hide(); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 8c436cee7f..8ade6d26be 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/service/floating_shortcut_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; import 'package:flutter/material.dart'; @@ -12,4 +13,15 @@ class FlowyService { // keyboard service final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service'); + + // floating toolbar service + final floatingShortcutServiceKey = + GlobalKey(debugLabel: 'flowy_floating_shortcut_service'); + FlowyFloatingShortCutService get floatingToolbarService { + assert(floatingShortcutServiceKey.currentState != null && + floatingShortcutServiceKey.currentState + is FlowyFloatingShortCutService); + return floatingShortcutServiceKey.currentState! + as FlowyFloatingShortCutService; + } } From fcb09e96367259d9a1951dad97dcc1141b3e3b9d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 25 Jul 2022 20:18:14 +0800 Subject: [PATCH 061/121] feat: typo and document --- .../flowy_editor/example/lib/main.dart | 17 +++++++-- .../example/lib/plugin/image_node_widget.dart | 6 ++-- .../lib/plugin/selected_text_node_widget.dart | 6 ++-- .../selection/floating_shortcut_widget.dart | 12 +++---- ...idget.dart => flowy_selection_widget.dart} | 0 .../lib/render/selection/selectable.dart | 36 +++++++++++++------ .../lib/service/editor_service.dart | 8 ++--- .../service/floating_shortcut_service.dart | 22 ++++++------ .../arrow_keys_handler.dart | 4 +-- .../delete_single_text_node_handler.dart | 2 +- .../shortcut_handler.dart | 2 +- .../lib/service/selection_service.dart | 8 ++--- .../flowy_editor/lib/service/service.dart | 8 ++--- 13 files changed, 80 insertions(+), 51 deletions(-) rename frontend/app_flowy/packages/flowy_editor/lib/render/selection/{selection_widget.dart => flowy_selection_widget.dart} (100%) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 6764c2faf1..9cc4d8c536 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -97,11 +97,22 @@ class _MyHomePageState extends State { return FlowyEditor( editorState: _editorState, keyEventHandler: const [], - shortCuts: [ + shortcuts: [ // TODO: this won't work, just a example for now. { - 'heading': (editorState, eventName) => - debugPrint('shortcut => $eventName') + 'h1': (editorState, eventName) { + debugPrint('shortcut => $eventName'); + final selectedNodes = editorState.selectedNodes; + if (selectedNodes.isEmpty) { + return; + } + final textNode = selectedNodes.first as TextNode; + TransactionBuilder(editorState) + ..formatText(textNode, 0, textNode.toRawString().length, { + 'heading': 'h1', + }) + ..commit(); + } }, { 'bold': (editorState, eventName) => diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 193cc879c9..8a9c96b22e 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -52,7 +52,7 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { } @override - TextSelection? getTextSelection() { + TextSelection? getCurrentTextSelection() { return null; } @@ -62,12 +62,12 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { } @override - Offset getLeftOfOffset() { + Offset getBackwardOffset() { return Offset.zero; } @override - Offset getRightOfOffset() { + Offset getForwardOffset() { return Offset.zero; } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index d827fdfde8..b234ecd967 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -100,7 +100,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - TextSelection? getTextSelection() { + TextSelection? getCurrentTextSelection() { return _textSelection; } @@ -111,7 +111,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - Offset getLeftOfOffset() { + Offset getBackwardOffset() { final textSelection = _textSelection; if (textSelection != null) { final leftTextSelection = TextSelection.collapsed( @@ -123,7 +123,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - Offset getRightOfOffset() { + Offset getForwardOffset() { final textSelection = _textSelection; if (textSelection != null) { final leftTextSelection = TextSelection.collapsed( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart index b91ed19fe2..9fbbbbcb01 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart @@ -1,9 +1,9 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; -typedef FloatingShortCutHandler = void Function( +typedef FloatingShortcutHandler = void Function( EditorState editorState, String eventName); -typedef FloatingShortCuts = List>; +typedef FloatingShortcuts = List>; class FloatingShortcutWidget extends StatelessWidget { const FloatingShortcutWidget({ @@ -17,11 +17,11 @@ class FloatingShortcutWidget extends StatelessWidget { final EditorState editorState; final LayerLink layerLink; final Rect rect; - final FloatingShortCuts floatingShortcuts; + final FloatingShortcuts floatingShortcuts; List get _shortcutNames => floatingShortcuts.map((shortcut) => shortcut.keys.first).toList(); - List get _shortcutHandlers => + List get _shortcutHandlers => floatingShortcuts.map((shortcut) => shortcut.values.first).toList(); @override @@ -38,7 +38,7 @@ class FloatingShortcutWidget extends StatelessWidget { itemCount: floatingShortcuts.length, itemBuilder: ((context, index) { final name = _shortcutNameInIndex(index); - final handler = _shortCutHandlerInIndex(index); + final handler = _shortcutHandlerInIndex(index); return Card( child: GestureDetector( onTap: () => handler(editorState, name), @@ -53,6 +53,6 @@ class FloatingShortcutWidget extends StatelessWidget { } String _shortcutNameInIndex(int index) => _shortcutNames[index]; - FloatingShortCutHandler _shortCutHandlerInIndex(int index) => + FloatingShortcutHandler _shortcutHandlerInIndex(int index) => _shortcutHandlers[index]; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 098c246569..6fc51049a1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -2,24 +2,40 @@ import 'package:flutter/material.dart'; /// mixin Selectable on State { - /// Returns a [Rect] list for overlay. - /// [start] and [end] are global offsets. - /// The return result must be an local offset. + /// Returns a [List] of the [Rect] selection sorrounded by start and end + /// in current widget. + /// + /// [start] and [end] are the offsets under the global coordinate system. + /// + /// The return result must be a [List] of the [Rect] + /// under the local coordinate system. List getSelectionRectsInRange(Offset start, Offset end); - /// Returns a [Rect] for cursor. - /// The return result must be an local offset. + /// Returns a [Rect] for the offset in current widget. + /// + /// [start] is the offset of the global coordination system. + /// + /// The return result must be an offset of the local coordinate system. Rect getCursorRect(Offset start); - /// Returns one unit offset to the left of the offset - Offset getLeftOfOffset(/* Cause */); + /// Returns a backward offset of the current offset based on the cause. + Offset getBackwardOffset(/* Cause */); - /// Returns one unit offset to the right of the offset - Offset getRightOfOffset(/* Cause */); + /// Returns a forward offset of the current offset based on the cause. + Offset getForwardOffset(/* Cause */); /// For [TextNode] only. - TextSelection? getTextSelection(); + /// + /// Returns a [TextSelection] or [Null]. + /// + /// Only the widget rendered by [TextNode] need to implement the detail, + /// and the rest can return null. + TextSelection? getCurrentTextSelection(); /// For [TextNode] only. + /// + /// Retruns a [Offset]. + /// Only the widget rendered by [TextNode] need to implement the detail, + /// and the rest can return [Offset.zero]. Offset getOffsetByTextSelection(TextSelection textSelection); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 0571cf4e03..7cd4eaf708 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -15,12 +15,12 @@ class FlowyEditor extends StatefulWidget { Key? key, required this.editorState, required this.keyEventHandler, - required this.shortCuts, + required this.shortcuts, }) : super(key: key); final EditorState editorState; final List keyEventHandler; - final FloatingShortCuts shortCuts; + final FloatingShortcuts shortcuts; @override State createState() => _FlowyEditorState(); @@ -44,11 +44,11 @@ class _FlowyEditorState extends State { ...widget.keyEventHandler, ], editorState: editorState, - child: FloatingShortCut( + child: FloatingShortcut( key: editorState.service.floatingShortcutServiceKey, size: const Size(200, 150), // TODO: support customize size. editorState: editorState, - floatingShortCuts: widget.shortCuts, + floatingShortcuts: widget.shortcuts, child: editorState.build(context), ), ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart index ed1a6a4528..774d906acc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart @@ -1,33 +1,35 @@ -import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; import 'package:flutter/material.dart'; -mixin FlowyFloatingShortCutService { +mixin FlowyFloatingShortcutService { + /// Show the floating shortcut widget beside the offset. void showInOffset(Offset offset, LayerLink layerLink); + + /// Hide the floating shortcut widget. void hide(); } -class FloatingShortCut extends StatefulWidget { - const FloatingShortCut({ +class FloatingShortcut extends StatefulWidget { + const FloatingShortcut({ Key? key, required this.size, required this.editorState, - required this.floatingShortCuts, + required this.floatingShortcuts, required this.child, }) : super(key: key); final Size size; final EditorState editorState; final Widget child; - final FloatingShortCuts floatingShortCuts; + final FloatingShortcuts floatingShortcuts; @override - State createState() => _FloatingShortCutState(); + State createState() => _FloatingShortcutState(); } -class _FloatingShortCutState extends State - with FlowyFloatingShortCutService { +class _FloatingShortcutState extends State + with FlowyFloatingShortcutService { OverlayEntry? _floatintShortcutOverlay; @override @@ -38,7 +40,7 @@ class _FloatingShortCutState extends State editorState: widget.editorState, layerLink: layerLink, rect: offset.translate(10, 0) & widget.size, - floatingShortcuts: widget.floatingShortCuts), + floatingShortcuts: widget.floatingShortcuts), ); Overlay.of(context)?.insert(_floatintShortcutOverlay!); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart index 4de5b61968..3049f54453 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart @@ -24,9 +24,9 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { final selectable = node?.key?.currentState?.unwrapOrNull(); Offset? offset; if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - offset = selectable?.getLeftOfOffset(); + offset = selectable?.getBackwardOffset(); } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - offset = selectable?.getRightOfOffset(); + offset = selectable?.getForwardOffset(); } final selectionService = editorState.service.selectionService; if (offset != null) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart index 1358276f47..db12d2bbb2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart @@ -17,7 +17,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { final node = selectionNodes.first.unwrapOrNull(); final selectable = node?.key?.currentState?.unwrapOrNull(); if (selectable != null) { - final textSelection = selectable.getTextSelection(); + final textSelection = selectable.getCurrentTextSelection(); if (textSelection != null) { if (textSelection.isCollapsed) { /// Three cases: diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart index 074e021f79..4e52d1bbe9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart @@ -17,7 +17,7 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { final textNode = selectedNodes.first.unwrapOrNull(); final selectable = textNode?.key?.currentState?.unwrapOrNull(); - final textSelection = selectable?.getTextSelection(); + final textSelection = selectable?.getCurrentTextSelection(); if (textNode != null && selectable != null && textSelection != null) { final offset = selectable.getOffsetByTextSelection(textSelection); final rect = selectable.getCursorRect(offset); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 778e657340..2f4bba86ec 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,5 +1,5 @@ import 'package:flowy_editor/render/selection/cursor_widget.dart'; -import 'package:flowy_editor/render/selection/selection_widget.dart'; +import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/service/floating_shortcut_service.dart'; import 'package:flutter/gestures.dart'; @@ -293,9 +293,9 @@ class _FlowySelectionState extends State } void _clearFloatingShorts() { - final shortCutService = editorState + final shortcutService = editorState .service.floatingShortcutServiceKey.currentState - ?.unwrapOrNull(); - shortCutService?.hide(); + ?.unwrapOrNull(); + shortcutService?.hide(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 8ade6d26be..7833e6d379 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -14,14 +14,14 @@ class FlowyService { // keyboard service final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service'); - // floating toolbar service + // floating shortcut service final floatingShortcutServiceKey = GlobalKey(debugLabel: 'flowy_floating_shortcut_service'); - FlowyFloatingShortCutService get floatingToolbarService { + FlowyFloatingShortcutService get floatingToolbarService { assert(floatingShortcutServiceKey.currentState != null && floatingShortcutServiceKey.currentState - is FlowyFloatingShortCutService); + is FlowyFloatingShortcutService); return floatingShortcutServiceKey.currentState! - as FlowyFloatingShortCutService; + as FlowyFloatingShortcutService; } } From 190728453476b64a6ffc3dd89c147b9dd240d5af Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 13:32:22 +0800 Subject: [PATCH 062/121] chore: add selection_service documentation and resymbol shortcut service. --- .../flowy_editor/example/lib/main.dart | 2 +- .../lib/extensions/node_extensions.dart | 11 +++ .../lib/service/editor_service.dart | 20 +++-- .../arrow_keys_handler.dart | 0 .../delete_nodes_handler.dart | 0 .../delete_single_text_node_handler.dart | 0 .../shortcut_handler.dart | 0 .../lib/service/selection_service.dart | 88 ++++++++++--------- .../flowy_editor/lib/service/service.dart | 2 +- ...cut_service.dart => shortcut_service.dart} | 0 10 files changed, 70 insertions(+), 53 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_key_event_handlers => internal_key_event_handlers}/arrow_keys_handler.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_key_event_handlers => internal_key_event_handlers}/delete_nodes_handler.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_key_event_handlers => internal_key_event_handlers}/delete_single_text_node_handler.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_key_event_handlers => internal_key_event_handlers}/shortcut_handler.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/service/{floating_shortcut_service.dart => shortcut_service.dart} (100%) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 9cc4d8c536..112c1dcd4f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -96,7 +96,7 @@ class _MyHomePageState extends State { ); return FlowyEditor( editorState: _editorState, - keyEventHandler: const [], + keyEventHandlers: const [], shortcuts: [ // TODO: this won't work, just a example for now. { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart new file mode 100644 index 0000000000..35cc18cdd2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart @@ -0,0 +1,11 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flutter/material.dart'; + +extension NodeExtensions on Node { + RenderBox? get renderBox => + key?.currentContext?.findRenderObject()?.unwrapOrNull(); + + Selectable? get selectable => key?.currentState?.unwrapOrNull(); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 7cd4eaf708..8b40981ccb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,25 +1,27 @@ import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; -import 'package:flowy_editor/service/floating_shortcut_service.dart'; -import 'package:flowy_editor/service/flowy_key_event_handlers/arrow_keys_handler.dart'; -import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart'; -import 'package:flowy_editor/service/flowy_key_event_handlers/shortcut_handler.dart'; +import 'package:flowy_editor/service/shortcut_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; +import 'package:flowy_editor/editor_state.dart'; -import '../editor_state.dart'; import 'package:flutter/material.dart'; class FlowyEditor extends StatefulWidget { const FlowyEditor({ Key? key, required this.editorState, - required this.keyEventHandler, + required this.keyEventHandlers, required this.shortcuts, }) : super(key: key); final EditorState editorState; - final List keyEventHandler; + final List keyEventHandlers; + + /// Shortcusts final FloatingShortcuts shortcuts; @override @@ -41,7 +43,7 @@ class _FlowyEditorState extends State { flowyDeleteNodesHandler, deleteSingleTextNodeHandler, arrowKeysHandler, - ...widget.keyEventHandler, + ...widget.keyEventHandlers, ], editorState: editorState, child: FloatingShortcut( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 2f4bba86ec..fa71536ecc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,7 +1,8 @@ import 'package:flowy_editor/render/selection/cursor_widget.dart'; import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; -import 'package:flowy_editor/service/floating_shortcut_service.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -11,58 +12,65 @@ import '../render/selection/selectable.dart'; /// Process selection and cursor mixin FlowySelectionService on State { - /// [Pan] and [Tap] must be mutually exclusive. - /// Pan - Offset? panStartOffset; - Offset? panEndOffset; - - /// Tap - Offset? tapOffset; - + /// [start] and [end] are the offsets under the global coordinate system. void updateSelection(Offset start, Offset end); + /// [start] is the offset under the global coordinate system. void updateCursor(Offset start); - /// Returns selected node(s) - /// Returns empty list if no nodes are being selected. + /// Returns selected [Node]s. Empty list would be returned + /// if no nodes are being selected. + /// + /// + /// [start] and [end] are the offsets under the global coordinate system. + /// + /// If end is not null, it means multiple selection, + /// otherwise single selection. List getSelectedNodes(Offset start, [Offset? end]); - /// Compute selected node triggered by [Tap] - Node? computeSelectedNodeInOffset( - Node node, - Offset offset, - ); + /// Return the [Node] or [Null] in single selection. + /// + /// [start] is the offset under the global coordinate system. + Node? computeSelectedNodeInOffset(Node node, Offset offset); - /// Compute selected nodes triggered by [Pan] + /// Return the [Node]s in multiple selection. Emtpy list would be returned + /// if no nodes are in range. + /// + /// [start] is the offset under the global coordinate system. List computeSelectedNodesInRange( Node node, Offset start, Offset end, ); - /// Pan + /// Return [bool] to identify the [Node] is in Range or not. + /// + /// [start] and [end] are the offsets under the global coordinate system. bool isNodeInSelection( Node node, Offset start, Offset end, ); - /// Tap - bool isNodeInOffset( - Node node, - Offset offset, - ); + /// Return [bool] to identify the [Node] contains [Offset] or not. + /// + /// [start] is the offset under the global coordinate system. + bool isNodeInOffset(Node node, Offset offset); } class FlowySelection extends StatefulWidget { const FlowySelection({ Key? key, + this.cursorColor = Colors.black, + this.selectionColor = const Color.fromARGB(60, 61, 61, 213), required this.editorState, required this.child, }) : super(key: key); final EditorState editorState; final Widget child; + final Color cursorColor; + final Color selectionColor; @override State createState() => _FlowySelectionState(); @@ -75,6 +83,14 @@ class _FlowySelectionState extends State final List _selectionOverlays = []; final List _cursorOverlays = []; + /// [Pan] and [Tap] must be mutually exclusive. + /// Pan + Offset? panStartOffset; + Offset? panEndOffset; + + /// Tap + Offset? tapOffset; + EditorState get editorState => widget.editorState; @override @@ -123,7 +139,7 @@ class _FlowySelectionState extends State for (final rect in selectionRects) { final overlay = OverlayEntry( builder: ((context) => SelectionWidget( - color: Colors.yellow.withAlpha(100), + color: widget.selectionColor, layerLink: node.layerLink, rect: rect, )), @@ -154,7 +170,7 @@ class _FlowySelectionState extends State builder: ((context) => CursorWidget( key: _cursorKey, rect: rect, - color: Colors.red, + color: widget.cursorColor, layerLink: selectedNode.layerLink, )), ); @@ -165,16 +181,10 @@ class _FlowySelectionState extends State @override List getSelectedNodes(Offset start, [Offset? end]) { if (end != null) { - return computeSelectedNodesInRange( - editorState.document.root, - start, - end, - ); + return computeSelectedNodesInRange(editorState.document.root, start, end); } else { - final reuslt = computeSelectedNodeInOffset( - editorState.document.root, - start, - ); + final reuslt = + computeSelectedNodeInOffset(editorState.document.root, start); if (reuslt != null) { return [reuslt]; } @@ -190,13 +200,11 @@ class _FlowySelectionState extends State return result; } } - if (node.parent != null && node.key != null) { if (isNodeInOffset(node, offset)) { return node; } } - return null; } @@ -217,9 +225,7 @@ class _FlowySelectionState extends State @override bool isNodeInOffset(Node node, Offset offset) { - assert(node.key != null); - final renderBox = - node.key?.currentContext?.findRenderObject() as RenderBox?; + final renderBox = node.renderBox; if (renderBox != null) { final boxOffset = renderBox.localToGlobal(Offset.zero); final boxRect = boxOffset & renderBox.size; @@ -230,9 +236,7 @@ class _FlowySelectionState extends State @override bool isNodeInSelection(Node node, Offset start, Offset end) { - assert(node.key != null); - final renderBox = - node.key?.currentContext?.findRenderObject() as RenderBox?; + final renderBox = node.renderBox; if (renderBox != null) { final rect = Rect.fromPoints(start, end); final boxOffset = renderBox.localToGlobal(Offset.zero); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 7833e6d379..f8cf4a9e5c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -1,4 +1,4 @@ -import 'package:flowy_editor/service/floating_shortcut_service.dart'; +import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart From 941671568e40f0cf87f10e3432ca45e3674f5f8a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 14:31:10 +0800 Subject: [PATCH 063/121] chore: remove unused import --- .../flowy_editor/example/lib/plugin/text_node_widget.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index cfb0ea5383..42bfd1e9f1 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -1,6 +1,5 @@ import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/document/text_delta.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; From 114ae2b45dd7b7b4cf42bd8ac11df1e188f5cfa1 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 18:40:53 +0800 Subject: [PATCH 064/121] feat: compute cursor and selection by [Selection] or [Offset] --- .../example/lib/plugin/image_node_widget.dart | 26 +- .../lib/plugin/selected_text_node_widget.dart | 65 +++-- .../flowy_editor/lib/document/path.dart | 26 ++ .../flowy_editor/lib/document/position.dart | 7 + .../flowy_editor/lib/document/selection.dart | 23 +- .../lib/render/selection/selectable.dart | 13 +- .../arrow_keys_handler.dart | 2 +- .../delete_single_text_node_handler.dart | 4 +- .../shortcut_handler.dart | 14 +- .../lib/service/selection_service.dart | 230 ++++++++++++------ 10 files changed, 270 insertions(+), 140 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 8a9c96b22e..fc440a8fa5 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,3 +1,5 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; @@ -38,27 +40,27 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { String get src => widget.node.attributes['image_src'] as String; @override - List getSelectionRectsInRange(Offset start, Offset end) { - final renderBox = context.findRenderObject() as RenderBox; - return [Offset.zero & renderBox.size]; + List getRectsInSelection(Selection selection) { + // TODO: implement getRectsInSelection + throw UnimplementedError(); } @override - Rect getCursorRect(Offset start) { - final renderBox = context.findRenderObject() as RenderBox; - final size = Size(2, renderBox.size.height); - final cursorOffset = Offset(renderBox.size.width, 0); - return cursorOffset & size; + Selection getSelectionInRange(Offset start, Offset end) { + // TODO: implement getSelectionInRange + throw UnimplementedError(); } @override - TextSelection? getCurrentTextSelection() { - return null; + Rect getCursorRectInPosition(Position position) { + // TODO: implement getCursorRectInPosition + throw UnimplementedError(); } @override - Offset getOffsetByTextSelection(TextSelection textSelection) { - return Offset.zero; + Position getPositionInOffset(Offset start) { + // TODO: implement getPositionInOffset + throw UnimplementedError(); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index b234ecd967..0f20f2fe3d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -1,6 +1,8 @@ import 'dart:math'; import 'package:example/plugin/debuggable_rich_text.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -56,49 +58,43 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> _textKey.currentContext?.findRenderObject() as RenderParagraph; @override - List getSelectionRectsInRange(Offset start, Offset end) { + Selection getSelectionInRange(Offset start, Offset end) { final localStart = _renderParagraph.globalToLocal(start); final localEnd = _renderParagraph.globalToLocal(end); - - var textSelection = - TextSelection(baseOffset: 0, extentOffset: node.toRawString().length); - // Returns select all if the start or end exceeds the size of the box - // TODO: don't need to compute everytime. - var rects = _computeSelectionRects(textSelection); - _textSelection = textSelection; - - if (localEnd.dy > localStart.dy) { - // downward - if (localEnd.dy >= rects.last.bottom) { - return rects; - } - } else { - // upward - if (localEnd.dy <= rects.first.top) { - return rects; - } - } - - final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; - final selectionExtentOffset = _getTextPositionAtOffset(localEnd).offset; - textSelection = TextSelection( - baseOffset: selectionBaseOffset, - extentOffset: selectionExtentOffset, + final baseOffset = _getTextPositionAtOffset(localStart).offset; + final extentOffset = _getTextPositionAtOffset(localEnd).offset; + return Selection.single( + path: node.path, + startOffset: baseOffset, + endOffset: extentOffset, + ); + } + + @override + List getRectsInSelection(Selection selection) { + assert(pathEquals(selection.start.path, selection.end.path)); + assert(pathEquals(selection.start.path, node.path)); + final textSelection = TextSelection( + baseOffset: selection.start.offset, + extentOffset: selection.end.offset, ); - _textSelection = textSelection; return _computeSelectionRects(textSelection); } @override - Rect getCursorRect(Offset start) { - final localStart = _renderParagraph.globalToLocal(start); - final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; - final textSelection = TextSelection.collapsed(offset: selectionBaseOffset); + Rect getCursorRectInPosition(Position position) { + final textSelection = TextSelection.collapsed(offset: position.offset); _textSelection = textSelection; - print('text selection = $textSelection'); return _computeCursorRect(textSelection.baseOffset); } + @override + Position getPositionInOffset(Offset start) { + final localStart = _renderParagraph.globalToLocal(start); + final baseOffset = _getTextPositionAtOffset(localStart).offset; + return Position(path: node.path, offset: baseOffset); + } + @override TextSelection? getCurrentTextSelection() { return _textSelection; @@ -175,8 +171,8 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _renderParagraph.getPositionForOffset(offset); } - List _computeSelectionRects(TextSelection selection) { - final textBoxes = _renderParagraph.getBoxesForSelection(selection); + List _computeSelectionRects(TextSelection textSelection) { + final textBoxes = _renderParagraph.getBoxesForSelection(textSelection); return textBoxes.map((box) => box.toRect()).toList(); } @@ -185,7 +181,6 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> final cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); final cursorHeight = _renderParagraph.getFullHeightForCaret(position); - print('offset = $offset, cursorHeight = $cursorHeight'); if (cursorHeight != null) { const cursorWidth = 2; return Rect.fromLTWH( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart index a8163f094d..bef96a7bd2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/foundation.dart'; typedef Path = List; @@ -5,3 +7,27 @@ typedef Path = List; bool pathEquals(Path path1, Path path2) { return listEquals(path1, path2); } + +/// Returns true if path1 >= path2, otherwise returns false. +/// TODO: Rename this function. +bool pathGreaterOrEquals(Path path1, Path path2) { + final length = min(path1.length, path2.length); + for (var i = 0; i < length; i++) { + if (path1[i] < path2[i]) { + return false; + } + } + return true; +} + +/// Returns true if path1 <= path2, otherwise returns false. +/// TODO: Rename this function. +bool pathLessOrEquals(Path path1, Path path2) { + final length = min(path1.length, path2.length); + for (var i = 0; i < length; i++) { + if (path1[i] > path2[i]) { + return false; + } + } + return true; +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart index 88941cd82e..e213c1eb33 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart @@ -24,4 +24,11 @@ class Position { final pathHash = hashList(path); return Object.hash(pathHash, offset); } + + Position copyWith({Path? path, int? offset}) { + return Position( + path: path ?? this.path, + offset: offset ?? this.offset, + ); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart index dea3a2b752..fe60e1abec 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -1,4 +1,5 @@ -import './position.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; class Selection { final Position start; @@ -9,9 +10,16 @@ class Selection { required this.end, }); - factory Selection.collapsed(Position pos) { - return Selection(start: pos, end: pos); - } + Selection.single({ + required Path path, + required int startOffset, + int? endOffset, + }) : start = Position(path: path, offset: startOffset), + end = Position(path: path, offset: endOffset ?? startOffset); + + Selection.collapsed(Position position) + : start = position, + end = position; Selection collapse({bool atStart = false}) { if (atStart) { @@ -24,4 +32,11 @@ class Selection { bool isCollapsed() { return start == end; } + + Selection copyWith({Position? start, Position? end}) { + return Selection( + start: start ?? this.start, + end: end ?? this.end, + ); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 6fc51049a1..f94d07e457 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -1,3 +1,5 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flutter/material.dart'; /// @@ -9,14 +11,17 @@ mixin Selectable on State { /// /// The return result must be a [List] of the [Rect] /// under the local coordinate system. - List getSelectionRectsInRange(Offset start, Offset end); + Selection getSelectionInRange(Offset start, Offset end); + + List getRectsInSelection(Selection selection); /// Returns a [Rect] for the offset in current widget. /// /// [start] is the offset of the global coordination system. /// /// The return result must be an offset of the local coordinate system. - Rect getCursorRect(Offset start); + Position getPositionInOffset(Offset start); + Rect getCursorRectInPosition(Position position); /// Returns a backward offset of the current offset based on the cause. Offset getBackwardOffset(/* Cause */); @@ -30,12 +35,12 @@ mixin Selectable on State { /// /// Only the widget rendered by [TextNode] need to implement the detail, /// and the rest can return null. - TextSelection? getCurrentTextSelection(); + TextSelection? getCurrentTextSelection() => null; /// For [TextNode] only. /// /// Retruns a [Offset]. /// Only the widget rendered by [TextNode] need to implement the detail, /// and the rest can return [Offset.zero]. - Offset getOffsetByTextSelection(TextSelection textSelection); + Offset getOffsetByTextSelection(TextSelection textSelection) => Offset.zero; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 3049f54453..44fc9a146f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -30,7 +30,7 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { } final selectionService = editorState.service.selectionService; if (offset != null) { - selectionService.updateCursor(offset); + // selectionService.updateCursor(offset); return KeyEventResult.handled; } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart index db12d2bbb2..47a83f314a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart @@ -37,7 +37,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { final newOfset = previousSelectable ?.getOffsetByTextSelection(newTextSelection); if (newOfset != null) { - selectionService.updateCursor(newOfset); + // selectionService.updateCursor(newOfset); } // merge TransactionBuilder(editorState) @@ -58,7 +58,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { final selectionService = editorState.service.selectionService; final newOfset = selectable.getOffsetByTextSelection(newTextSelection); - selectionService.updateCursor(newOfset); + // selectionService.updateCursor(newOfset); return KeyEventResult.handled; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart index 4e52d1bbe9..3eef8c1d1b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart @@ -18,13 +18,13 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { final textNode = selectedNodes.first.unwrapOrNull(); final selectable = textNode?.key?.currentState?.unwrapOrNull(); final textSelection = selectable?.getCurrentTextSelection(); - if (textNode != null && selectable != null && textSelection != null) { - final offset = selectable.getOffsetByTextSelection(textSelection); - final rect = selectable.getCursorRect(offset); - editorState.service.floatingToolbarService - .showInOffset(rect.topLeft, textNode.layerLink); - return KeyEventResult.handled; - } + // if (textNode != null && selectable != null && textSelection != null) { + // final offset = selectable.getOffsetByTextSelection(textSelection); + // final rect = selectable.getCursorRect(offset); + // editorState.service.floatingToolbarService + // .showInOffset(rect.topLeft, textNode.layerLink); + // return KeyEventResult.handled; + // } return KeyEventResult.ignored; }; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index fa71536ecc..8f6ac6d6ee 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,3 +1,6 @@ +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/render/selection/cursor_widget.dart'; import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; @@ -12,11 +15,8 @@ import '../render/selection/selectable.dart'; /// Process selection and cursor mixin FlowySelectionService on State { - /// [start] and [end] are the offsets under the global coordinate system. - void updateSelection(Offset start, Offset end); - - /// [start] is the offset under the global coordinate system. - void updateCursor(Offset start); + /// + void updateSelection(Selection selection); /// Returns selected [Node]s. Empty list would be returned /// if no nodes are being selected. @@ -26,18 +26,21 @@ mixin FlowySelectionService on State { /// /// If end is not null, it means multiple selection, /// otherwise single selection. - List getSelectedNodes(Offset start, [Offset? end]); + List getNodesInRange(Offset start, [Offset? end]); + + /// + List getNodesInSelection(Selection selection); /// Return the [Node] or [Null] in single selection. /// /// [start] is the offset under the global coordinate system. - Node? computeSelectedNodeInOffset(Node node, Offset offset); + Node? computeNodeInOffset(Node node, Offset offset); /// Return the [Node]s in multiple selection. Emtpy list would be returned /// if no nodes are in range. /// /// [start] is the offset under the global coordinate system. - List computeSelectedNodesInRange( + List computeNodesInRange( Node node, Offset start, Offset end, @@ -93,6 +96,10 @@ class _FlowySelectionState extends State EditorState get editorState => widget.editorState; + @override + List getNodesInSelection(Selection selection) => + _selectedNodesInSelection(editorState.document.root, selection); + @override Widget build(BuildContext context) { return RawGestureDetector( @@ -121,70 +128,23 @@ class _FlowySelectionState extends State } @override - void updateSelection(Offset start, Offset end) { + void updateSelection(Selection selection) { _clearAllOverlayEntries(); - final nodes = getSelectedNodes(start, end); - editorState.selectedNodes = nodes; - if (nodes.isEmpty) { - return; - } - - for (final node in nodes) { - if (node.key?.currentState is! Selectable) { - continue; - } - final selectable = node.key?.currentState as Selectable; - final selectionRects = selectable.getSelectionRectsInRange(start, end); - for (final rect in selectionRects) { - final overlay = OverlayEntry( - builder: ((context) => SelectionWidget( - color: widget.selectionColor, - layerLink: node.layerLink, - rect: rect, - )), - ); - _selectionOverlays.add(overlay); - } - } - Overlay.of(context)?.insertAll(_selectionOverlays); - } - - @override - void updateCursor(Offset start) { - _clearAllOverlayEntries(); - - final nodes = getSelectedNodes(start); - editorState.selectedNodes = nodes; - if (nodes.isEmpty) { - return; - } - - final selectedNode = nodes.first; - if (selectedNode.key?.currentState is! Selectable) { - return; - } - final selectable = selectedNode.key?.currentState as Selectable; - final rect = selectable.getCursorRect(start); - final cursor = OverlayEntry( - builder: ((context) => CursorWidget( - key: _cursorKey, - rect: rect, - color: widget.cursorColor, - layerLink: selectedNode.layerLink, - )), - ); - _cursorOverlays.add(cursor); - Overlay.of(context)?.insertAll(_cursorOverlays); - } - - @override - List getSelectedNodes(Offset start, [Offset? end]) { - if (end != null) { - return computeSelectedNodesInRange(editorState.document.root, start, end); + // cursor + if (selection.isCollapsed()) { + _updateCursor(selection.start); } else { - final reuslt = - computeSelectedNodeInOffset(editorState.document.root, start); + _updateSelection(selection); + } + } + + @override + List getNodesInRange(Offset start, [Offset? end]) { + if (end != null) { + return computeNodesInRange(editorState.document.root, start, end); + } else { + final reuslt = computeNodeInOffset(editorState.document.root, start); if (reuslt != null) { return [reuslt]; } @@ -193,9 +153,9 @@ class _FlowySelectionState extends State } @override - Node? computeSelectedNodeInOffset(Node node, Offset offset) { + Node? computeNodeInOffset(Node node, Offset offset) { for (final child in node.children) { - final result = computeSelectedNodeInOffset(child, offset); + final result = computeNodeInOffset(child, offset); if (result != null) { return result; } @@ -209,7 +169,7 @@ class _FlowySelectionState extends State } @override - List computeSelectedNodesInRange(Node node, Offset start, Offset end) { + List computeNodesInRange(Node node, Offset start, Offset end) { List result = []; if (node.parent != null && node.key != null) { if (isNodeInSelection(node, start, end)) { @@ -217,7 +177,7 @@ class _FlowySelectionState extends State } } for (final child in node.children) { - result.addAll(computeSelectedNodesInRange(child, start, end)); + result.addAll(computeNodesInRange(child, start, end)); } // TODO: sort the result return result; @@ -254,7 +214,16 @@ class _FlowySelectionState extends State panStartOffset = null; panEndOffset = null; - updateCursor(tapOffset!); + final nodes = getNodesInRange(tapOffset!); + if (nodes.isNotEmpty) { + assert(nodes.length == 1); + final selectable = nodes.first.selectable; + if (selectable != null) { + final position = selectable.getPositionInOffset(tapOffset!); + final selection = Selection.collapsed(position); + updateSelection(selection); + } + } } void _onPanStart(DragStartDetails details) { @@ -271,7 +240,16 @@ class _FlowySelectionState extends State panEndOffset = details.globalPosition; tapOffset = null; - updateSelection(panStartOffset!, panEndOffset!); + final nodes = getNodesInRange(panStartOffset!, panEndOffset!); + final first = nodes.first.selectable; + final last = nodes.last.selectable; + if (first != null && last != null) { + final selection = Selection( + start: first.getSelectionInRange(panStartOffset!, panEndOffset!).start, + end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, + ); + updateSelection(selection); + } } void _onPanEnd(DragEndDetails details) { @@ -302,4 +280,106 @@ class _FlowySelectionState extends State ?.unwrapOrNull(); shortcutService?.hide(); } + + void _updateSelection(Selection selection) { + final nodes = + _selectedNodesInSelection(editorState.document.root, selection); + + var index = 0; + for (final node in nodes) { + final selectable = node.selectable; + if (selectable == null) { + continue; + } + + Selection newSelection; + if (node is TextNode) { + if (pathEquals(selection.start.path, selection.end.path)) { + newSelection = selection.copyWith(); + } else { + if (index == 0) { + newSelection = selection.copyWith( + /// FIXME: make it better. + end: selection.start.copyWith(offset: node.toRawString().length), + ); + } else if (index == nodes.length - 1) { + newSelection = selection.copyWith( + /// FIXME: make it better. + start: selection.end.copyWith(offset: 0), + ); + } else { + final position = Position(path: node.path); + newSelection = Selection( + start: position.copyWith(offset: 0), + end: position.copyWith(offset: node.toRawString().length), + ); + } + } + } else { + newSelection = Selection.collapsed( + Position(path: node.path), + ); + } + + final rects = selectable.getRectsInSelection(newSelection); + + for (final rect in rects) { + final overlay = OverlayEntry( + builder: ((context) => SelectionWidget( + color: widget.selectionColor, + layerLink: node.layerLink, + rect: rect, + )), + ); + _selectionOverlays.add(overlay); + } + index += 1; + } + Overlay.of(context)?.insertAll(_selectionOverlays); + } + + void _updateCursor(Position position) { + final node = _selectedNodeInPostion(editorState.document.root, position); + + assert(node != null); + if (node == null) { + return; + } + + final selectable = node.selectable; + final rect = selectable?.getCursorRectInPosition(position); + if (rect != null) { + final cursor = OverlayEntry( + builder: ((context) => CursorWidget( + key: _cursorKey, + rect: rect, + color: widget.cursorColor, + layerLink: node.layerLink, + )), + ); + _cursorOverlays.add(cursor); + Overlay.of(context)?.insertAll(_cursorOverlays); + } + } + + List _selectedNodesInSelection(Node node, Selection selection) { + List result = []; + if (node.parent != null) { + if (_isNodeInSelection(node, selection)) { + result.add(node); + } + } + for (final child in node.children) { + result.addAll(_selectedNodesInSelection(child, selection)); + } + return result; + } + + Node? _selectedNodeInPostion(Node node, Position position) => + node.childAtPath(position.path); + + bool _isNodeInSelection(Node node, Selection selection) { + return pathGreaterOrEquals(node.path, selection.start.path) && + pathLessOrEquals(node.path, selection.end.path); + } } From cde2127dec91c473eb42bfd9e172fc7817c18e05 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 20:10:47 +0800 Subject: [PATCH 065/121] feat: compute cursor and selection by [Selection] or [Offset] --- .../example/lib/plugin/text_node_widget.dart | 2 +- .../flowy_editor/lib/document/path.dart | 24 ---- .../flowy_editor/lib/document/position.dart | 3 + .../flowy_editor/lib/document/selection.dart | 12 +- .../lib/extensions/node_extensions.dart | 12 ++ .../lib/extensions/path_extensions.dart | 25 ++++ .../lib/service/selection_service.dart | 127 +++++++++++------- .../flowy_editor/test/flowy_editor_test.dart | 2 +- 8 files changed, 131 insertions(+), 76 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 42bfd1e9f1..a67ebcd2ad 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -326,7 +326,7 @@ TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) { if (!pathEquals(nodePath, globalSel.start.path)) { return null; } - if (globalSel.isCollapsed()) { + if (globalSel.isCollapsed) { return TextSelection( baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset); } else { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart index bef96a7bd2..8f24947649 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart @@ -7,27 +7,3 @@ typedef Path = List; bool pathEquals(Path path1, Path path2) { return listEquals(path1, path2); } - -/// Returns true if path1 >= path2, otherwise returns false. -/// TODO: Rename this function. -bool pathGreaterOrEquals(Path path1, Path path2) { - final length = min(path1.length, path2.length); - for (var i = 0; i < length; i++) { - if (path1[i] < path2[i]) { - return false; - } - } - return true; -} - -/// Returns true if path1 <= path2, otherwise returns false. -/// TODO: Rename this function. -bool pathLessOrEquals(Path path1, Path path2) { - final length = min(path1.length, path2.length); - for (var i = 0; i < length; i++) { - if (path1[i] > path2[i]) { - return false; - } - } - return true; -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart index e213c1eb33..a60f04e89b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart @@ -31,4 +31,7 @@ class Position { offset: offset ?? this.offset, ); } + + @override + String toString() => 'path = $path, offset = $offset'; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart index fe60e1abec..1734fabf24 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -1,5 +1,6 @@ import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/extensions/path_extensions.dart'; class Selection { final Position start; @@ -29,9 +30,11 @@ class Selection { } } - bool isCollapsed() { - return start == end; - } + bool get isCollapsed => start == end; + bool get isUpward => + start.path >= end.path && !pathEquals(start.path, end.path); + bool get isDownward => + start.path <= end.path && !pathEquals(start.path, end.path); Selection copyWith({Position? start, Position? end}) { return Selection( @@ -39,4 +42,7 @@ class Selection { end: end ?? this.end, ); } + + @override + String toString() => '[Selection] start = $start, end = $end'; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart index 35cc18cdd2..49cc38f749 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart @@ -1,5 +1,9 @@ +import 'dart:math'; + import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flowy_editor/extensions/path_extensions.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flutter/material.dart'; @@ -8,4 +12,12 @@ extension NodeExtensions on Node { key?.currentContext?.findRenderObject()?.unwrapOrNull(); Selectable? get selectable => key?.currentState?.unwrapOrNull(); + + bool inSelection(Selection selection) { + if (selection.start.path <= selection.end.path) { + return selection.start.path <= path && path <= selection.end.path; + } else { + return selection.end.path <= path && path <= selection.start.path; + } + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart new file mode 100644 index 0000000000..b37d846482 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart @@ -0,0 +1,25 @@ +import 'package:flowy_editor/document/path.dart'; + +import 'dart:math'; + +extension PathExtensions on Path { + bool operator >=(Path other) { + final length = min(this.length, other.length); + for (var i = 0; i < length; i++) { + if (this[i] < other[i]) { + return false; + } + } + return true; + } + + bool operator <=(Path other) { + final length = min(this.length, other.length); + for (var i = 0; i < length; i++) { + if (this[i] > other[i]) { + return false; + } + } + return true; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 8f6ac6d6ee..e118f5ea62 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,4 +1,5 @@ import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/render/selection/cursor_widget.dart'; @@ -6,18 +7,22 @@ import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flowy_editor/service/shortcut_service.dart'; +import 'package:flowy_editor/editor_state.dart'; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import '../editor_state.dart'; -import '../document/node.dart'; -import '../render/selection/selectable.dart'; - /// Process selection and cursor mixin FlowySelectionService on State { + /// + List get currentSelectedNodes; + /// void updateSelection(Selection selection); + /// + void clearSelection(); + /// Returns selected [Node]s. Empty list would be returned /// if no nodes are being selected. /// @@ -49,7 +54,7 @@ mixin FlowySelectionService on State { /// Return [bool] to identify the [Node] is in Range or not. /// /// [start] and [end] are the offsets under the global coordinate system. - bool isNodeInSelection( + bool isNodeInRange( Node node, Offset start, Offset end, @@ -96,6 +101,12 @@ class _FlowySelectionState extends State EditorState get editorState => widget.editorState; + Node? _selectedNodeInPostion(Node node, Position position) => + node.childAtPath(position.path); + + @override + List currentSelectedNodes = []; + @override List getNodesInSelection(Selection selection) => _selectedNodesInSelection(editorState.document.root, selection); @@ -129,16 +140,21 @@ class _FlowySelectionState extends State @override void updateSelection(Selection selection) { - _clearAllOverlayEntries(); + _clearSelection(); // cursor - if (selection.isCollapsed()) { + if (selection.isCollapsed) { _updateCursor(selection.start); } else { _updateSelection(selection); } } + @override + void clearSelection() { + _clearSelection(); + } + @override List getNodesInRange(Offset start, [Offset? end]) { if (end != null) { @@ -172,7 +188,7 @@ class _FlowySelectionState extends State List computeNodesInRange(Node node, Offset start, Offset end) { List result = []; if (node.parent != null && node.key != null) { - if (isNodeInSelection(node, start, end)) { + if (isNodeInRange(node, start, end)) { result.add(node); } } @@ -195,7 +211,7 @@ class _FlowySelectionState extends State } @override - bool isNodeInSelection(Node node, Offset start, Offset end) { + bool isNodeInRange(Node node, Offset start, Offset end) { final renderBox = node.renderBox; if (renderBox != null) { final rect = Rect.fromPoints(start, end); @@ -244,10 +260,21 @@ class _FlowySelectionState extends State final first = nodes.first.selectable; final last = nodes.last.selectable; if (first != null && last != null) { - final selection = Selection( - start: first.getSelectionInRange(panStartOffset!, panEndOffset!).start, - end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, - ); + final Selection selection; + if (panStartOffset!.dy <= panEndOffset!.dy) { + // down + selection = Selection( + start: + first.getSelectionInRange(panStartOffset!, panEndOffset!).start, + end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, + ); + } else { + // up + selection = Selection( + start: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, + end: first.getSelectionInRange(panStartOffset!, panEndOffset!).start, + ); + } updateSelection(selection); } } @@ -256,35 +283,29 @@ class _FlowySelectionState extends State // do nothing } - void _clearAllOverlayEntries() { - _clearSelection(); - _clearCursor(); - _clearFloatingShorts(); - } - void _clearSelection() { + currentSelectedNodes = []; + + // clear selection _selectionOverlays ..forEach((overlay) => overlay.remove()) ..clear(); - } - - void _clearCursor() { + // clear cursors _cursorOverlays ..forEach((overlay) => overlay.remove()) ..clear(); - } - - void _clearFloatingShorts() { - final shortcutService = editorState - .service.floatingShortcutServiceKey.currentState - ?.unwrapOrNull(); - shortcutService?.hide(); + // clear floating shortcusts + editorState.service.floatingShortcutServiceKey.currentState + ?.unwrapOrNull() + ?.hide(); } void _updateSelection(Selection selection) { final nodes = _selectedNodesInSelection(editorState.document.root, selection); + currentSelectedNodes = nodes; + var index = 0; for (final node in nodes) { final selectable = node.selectable; @@ -293,20 +314,38 @@ class _FlowySelectionState extends State } Selection newSelection; + // TODO: too complicate, need to refactor. if (node is TextNode) { if (pathEquals(selection.start.path, selection.end.path)) { newSelection = selection.copyWith(); } else { if (index == 0) { - newSelection = selection.copyWith( - /// FIXME: make it better. - end: selection.start.copyWith(offset: node.toRawString().length), - ); + if (selection.isUpward) { + newSelection = selection.copyWith( + /// FIXME: make it better. + start: selection.end.copyWith(), + end: selection.end.copyWith(offset: node.toRawString().length), + ); + } else { + newSelection = selection.copyWith( + /// FIXME: make it better. + end: + selection.start.copyWith(offset: node.toRawString().length), + ); + } } else if (index == nodes.length - 1) { - newSelection = selection.copyWith( - /// FIXME: make it better. - start: selection.end.copyWith(offset: 0), - ); + if (selection.isUpward) { + newSelection = selection.copyWith( + /// FIXME: make it better. + start: selection.start.copyWith(offset: 0), + end: selection.start.copyWith(), + ); + } else { + newSelection = selection.copyWith( + /// FIXME: make it better. + start: selection.end.copyWith(offset: 0), + ); + } } else { final position = Position(path: node.path); newSelection = Selection( @@ -339,13 +378,15 @@ class _FlowySelectionState extends State } void _updateCursor(Position position) { - final node = _selectedNodeInPostion(editorState.document.root, position); + final node = editorState.document.root.childAtPath(position.path); assert(node != null); if (node == null) { return; } + currentSelectedNodes = [node]; + final selectable = node.selectable; final rect = selectable?.getCursorRectInPosition(position); if (rect != null) { @@ -365,7 +406,7 @@ class _FlowySelectionState extends State List _selectedNodesInSelection(Node node, Selection selection) { List result = []; if (node.parent != null) { - if (_isNodeInSelection(node, selection)) { + if (node.inSelection(selection)) { result.add(node); } } @@ -374,12 +415,4 @@ class _FlowySelectionState extends State } return result; } - - Node? _selectedNodeInPostion(Node node, Position position) => - node.childAtPath(position.path); - - bool _isNodeInSelection(Node node, Selection selection) { - return pathGreaterOrEquals(node.path, selection.start.path) && - pathLessOrEquals(node.path, selection.end.path); - } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index d272364b44..16ccadb079 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -127,7 +127,7 @@ void main() { final pos = Position(path: [0], offset: 0); final sel = Selection.collapsed(pos); expect(sel.start, sel.end); - expect(sel.isCollapsed(), true); + expect(sel.isCollapsed, true); }); test('test selection collapse', () { From c048c8f623451ea7fbbb4499b90846104826420c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 23:28:51 +0800 Subject: [PATCH 066/121] fix: compute wrong upward selection --- .../example/lib/plugin/image_node_widget.dart | 22 +-- .../lib/plugin/selected_text_node_widget.dart | 23 +-- .../flowy_editor/lib/document/selection.dart | 3 + .../lib/render/selection/selectable.dart | 7 +- .../arrow_keys_handler.dart | 23 --- .../lib/service/selection_service.dart | 137 ++++++++---------- 6 files changed, 83 insertions(+), 132 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index fc440a8fa5..c5084df2fb 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -39,6 +39,18 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { EditorState get editorState => widget.editorState; String get src => widget.node.attributes['image_src'] as String; + @override + Position end() { + // TODO: implement end + throw UnimplementedError(); + } + + @override + Position start() { + // TODO: implement start + throw UnimplementedError(); + } + @override List getRectsInSelection(Selection selection) { // TODO: implement getRectsInSelection @@ -63,16 +75,6 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { throw UnimplementedError(); } - @override - Offset getBackwardOffset() { - return Offset.zero; - } - - @override - Offset getForwardOffset() { - return Offset.zero; - } - @override Widget build(BuildContext context) { return _build(context); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 0f20f2fe3d..894f6b1848 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -107,28 +107,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - Offset getBackwardOffset() { - final textSelection = _textSelection; - if (textSelection != null) { - final leftTextSelection = TextSelection.collapsed( - offset: max(0, textSelection.baseOffset - 1), - ); - return getOffsetByTextSelection(leftTextSelection); - } - return Offset.zero; - } + Position start() => Position(path: node.path, offset: 0); @override - Offset getForwardOffset() { - final textSelection = _textSelection; - if (textSelection != null) { - final leftTextSelection = TextSelection.collapsed( - offset: min(node.toRawString().length, textSelection.extentOffset + 1), - ); - return getOffsetByTextSelection(leftTextSelection); - } - return Offset.zero; - } + Position end() => + Position(path: node.path, offset: node.toRawString().length); @override Widget build(BuildContext context) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart index 1734fabf24..a3919a21f6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -31,6 +31,7 @@ class Selection { } bool get isCollapsed => start == end; + bool get isSingle => pathEquals(start.path, end.path); bool get isUpward => start.path >= end.path && !pathEquals(start.path, end.path); bool get isDownward => @@ -43,6 +44,8 @@ class Selection { ); } + Selection copy() => Selection(start: start, end: end); + @override String toString() => '[Selection] start = $start, end = $end'; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index f94d07e457..4d155972df 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -23,11 +23,8 @@ mixin Selectable on State { Position getPositionInOffset(Offset start); Rect getCursorRectInPosition(Position position); - /// Returns a backward offset of the current offset based on the cause. - Offset getBackwardOffset(/* Cause */); - - /// Returns a forward offset of the current offset based on the cause. - Offset getForwardOffset(/* Cause */); + Position start(); + Position end(); /// For [TextNode] only. /// diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 44fc9a146f..95496db2ea 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,5 +1,3 @@ -import 'package:flowy_editor/extensions/object_extensions.dart'; -import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,26 +10,5 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { return KeyEventResult.ignored; } - // TODO: Up and Down - - // Left and Right - final selectedNodes = editorState.selectedNodes; - if (selectedNodes.length != 1) { - return KeyEventResult.ignored; - } - - final node = selectedNodes.first.unwrapOrNull(); - final selectable = node?.key?.currentState?.unwrapOrNull(); - Offset? offset; - if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - offset = selectable?.getBackwardOffset(); - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - offset = selectable?.getForwardOffset(); - } - final selectionService = editorState.service.selectionService; - if (offset != null) { - // selectionService.updateCursor(offset); - return KeyEventResult.handled; - } return KeyEventResult.ignored; }; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index e118f5ea62..19604b0227 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -14,15 +14,26 @@ import 'package:flutter/material.dart'; /// Process selection and cursor mixin FlowySelectionService on State { + /// Returns the currently selected [Node]s. /// + /// The order of the return is determined according to the selected order. List get currentSelectedNodes; + /// ------------------ Selection ------------------------ + /// void updateSelection(Selection selection); /// void clearSelection(); + /// + List getNodesInSelection(Selection selection); + + /// ------------------ Selection ------------------------ + + /// ------------------ Offset ------------------------ + /// Returns selected [Node]s. Empty list would be returned /// if no nodes are being selected. /// @@ -33,9 +44,6 @@ mixin FlowySelectionService on State { /// otherwise single selection. List getNodesInRange(Offset start, [Offset? end]); - /// - List getNodesInSelection(Selection selection); - /// Return the [Node] or [Null] in single selection. /// /// [start] is the offset under the global coordinate system. @@ -64,6 +72,8 @@ mixin FlowySelectionService on State { /// /// [start] is the offset under the global coordinate system. bool isNodeInOffset(Node node, Offset offset); + + /// ------------------ Offset ------------------------ } class FlowySelection extends StatefulWidget { @@ -101,9 +111,6 @@ class _FlowySelectionState extends State EditorState get editorState => widget.editorState; - Node? _selectedNodeInPostion(Node node, Position position) => - node.childAtPath(position.path); - @override List currentSelectedNodes = []; @@ -186,6 +193,17 @@ class _FlowySelectionState extends State @override List computeNodesInRange(Node node, Offset start, Offset end) { + final result = _computeNodesInRange(node, start, end); + if (start.dy <= end.dy) { + // downward + return result; + } else { + // upward + return result.reversed.toList(growable: false); + } + } + + List _computeNodesInRange(Node node, Offset start, Offset end) { List result = []; if (node.parent != null && node.key != null) { if (isNodeInRange(node, start, end)) { @@ -195,7 +213,6 @@ class _FlowySelectionState extends State for (final child in node.children) { result.addAll(computeNodesInRange(child, start, end)); } - // TODO: sort the result return result; } @@ -223,13 +240,12 @@ class _FlowySelectionState extends State } void _onTapDown(TapDownDetails details) { - debugPrint('on tap down'); - - // TODO: use setter to make them exclusive?? - tapOffset = details.globalPosition; + // clear old state. panStartOffset = null; panEndOffset = null; + tapOffset = details.globalPosition; + final nodes = getNodesInRange(tapOffset!); if (nodes.isNotEmpty) { assert(nodes.length == 1); @@ -243,38 +259,30 @@ class _FlowySelectionState extends State } void _onPanStart(DragStartDetails details) { - debugPrint('on pan start'); - - panStartOffset = details.globalPosition; + // clear old state. panEndOffset = null; tapOffset = null; + clearSelection(); + + panStartOffset = details.globalPosition; } void _onPanUpdate(DragUpdateDetails details) { - // debugPrint('on pan update'); - panEndOffset = details.globalPosition; - tapOffset = null; final nodes = getNodesInRange(panStartOffset!, panEndOffset!); final first = nodes.first.selectable; final last = nodes.last.selectable; + + // compute the selection in range. if (first != null && last != null) { - final Selection selection; - if (panStartOffset!.dy <= panEndOffset!.dy) { - // down - selection = Selection( - start: - first.getSelectionInRange(panStartOffset!, panEndOffset!).start, - end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, - ); - } else { - // up - selection = Selection( - start: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, - end: first.getSelectionInRange(panStartOffset!, panEndOffset!).start, - ); - } + bool isDownward = panStartOffset!.dy <= panEndOffset!.dy; + final start = + first.getSelectionInRange(panStartOffset!, panEndOffset!).start; + final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end; + final selection = Selection( + start: isDownward ? start : end, end: isDownward ? end : start); + debugPrint('[_onPanUpdate] $selection'); updateSelection(selection); } } @@ -313,51 +321,32 @@ class _FlowySelectionState extends State continue; } - Selection newSelection; - // TODO: too complicate, need to refactor. - if (node is TextNode) { - if (pathEquals(selection.start.path, selection.end.path)) { - newSelection = selection.copyWith(); - } else { - if (index == 0) { - if (selection.isUpward) { - newSelection = selection.copyWith( - /// FIXME: make it better. - start: selection.end.copyWith(), - end: selection.end.copyWith(offset: node.toRawString().length), - ); - } else { - newSelection = selection.copyWith( - /// FIXME: make it better. - end: - selection.start.copyWith(offset: node.toRawString().length), - ); - } - } else if (index == nodes.length - 1) { - if (selection.isUpward) { - newSelection = selection.copyWith( - /// FIXME: make it better. - start: selection.start.copyWith(offset: 0), - end: selection.start.copyWith(), - ); - } else { - newSelection = selection.copyWith( - /// FIXME: make it better. - start: selection.end.copyWith(offset: 0), - ); - } + var newSelection = selection.copy(); + // In the case of multiple selections, + // we need to return a new selection for each selected node individually. + if (!selection.isSingle) { + // <> means selected. + // text: abcdopqr + if (index == 0) { + if (selection.isDownward) { + newSelection = selection.copyWith(end: selectable.end()); } else { - final position = Position(path: node.path); - newSelection = Selection( - start: position.copyWith(offset: 0), - end: position.copyWith(offset: node.toRawString().length), - ); + newSelection = selection.copyWith(start: selectable.start()); } + } else if (index == nodes.length - 1) { + if (selection.isDownward) { + newSelection = selection.copyWith(start: selectable.start()); + } else { + newSelection = selection.copyWith(end: selectable.end()); + } + } else { + newSelection = selection.copyWith( + start: selectable.start(), + end: selectable.end(), + ); } - } else { - newSelection = Selection.collapsed( - Position(path: node.path), - ); } final rects = selectable.getRectsInSelection(newSelection); From 155b675dbe3382c976421d443dc5f8f46c281663 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 27 Jul 2022 10:56:30 +0800 Subject: [PATCH 067/121] feat: implement input service(alpha) --- .../expandable_floating_action_button.dart | 234 ++++++++++++++++++ .../flowy_editor/example/lib/main.dart | 136 ++++++---- .../lib/service/editor_service.dart | 33 +-- .../lib/service/input_service.dart | 179 ++++++++++++++ .../lib/service/selection_service.dart | 17 +- .../flowy_editor/lib/service/service.dart | 3 + 6 files changed, 537 insertions(+), 65 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/expandable_floating_action_button.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/expandable_floating_action_button.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/expandable_floating_action_button.dart new file mode 100644 index 0000000000..01da3ab593 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/expandable_floating_action_button.dart @@ -0,0 +1,234 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +// copy from https://docs.flutter.dev/cookbook/effects/expandable-fab +@immutable +class ExpandableFab extends StatefulWidget { + const ExpandableFab({ + super.key, + this.initialOpen, + required this.distance, + required this.children, + }); + + final bool? initialOpen; + final double distance; + final List children; + + @override + State createState() => _ExpandableFabState(); +} + +class _ExpandableFabState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _expandAnimation; + bool _open = false; + + @override + void initState() { + super.initState(); + _open = widget.initialOpen ?? false; + _controller = AnimationController( + value: _open ? 1.0 : 0.0, + duration: const Duration(milliseconds: 250), + vsync: this, + ); + _expandAnimation = CurvedAnimation( + curve: Curves.fastOutSlowIn, + reverseCurve: Curves.easeOutQuad, + parent: _controller, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _toggle() { + setState(() { + _open = !_open; + if (_open) { + _controller.forward(); + } else { + _controller.reverse(); + } + }); + } + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: Stack( + alignment: Alignment.bottomRight, + clipBehavior: Clip.none, + children: [ + _buildTapToCloseFab(), + ..._buildExpandingActionButtons(), + _buildTapToOpenFab(), + ], + ), + ); + } + + Widget _buildTapToCloseFab() { + return SizedBox( + width: 56.0, + height: 56.0, + child: Center( + child: Material( + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + elevation: 4.0, + child: InkWell( + onTap: _toggle, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.close, + color: Theme.of(context).primaryColor, + ), + ), + ), + ), + ), + ); + } + + List _buildExpandingActionButtons() { + final children = []; + final count = widget.children.length; + final step = 90.0 / (count - 1); + for (var i = 0, angleInDegrees = 0.0; + i < count; + i++, angleInDegrees += step) { + children.add( + _ExpandingActionButton( + directionInDegrees: angleInDegrees, + maxDistance: widget.distance, + progress: _expandAnimation, + child: widget.children[i], + ), + ); + } + return children; + } + + Widget _buildTapToOpenFab() { + return IgnorePointer( + ignoring: _open, + child: AnimatedContainer( + transformAlignment: Alignment.center, + transform: Matrix4.diagonal3Values( + _open ? 0.7 : 1.0, + _open ? 0.7 : 1.0, + 1.0, + ), + duration: const Duration(milliseconds: 250), + curve: const Interval(0.0, 0.5, curve: Curves.easeOut), + child: AnimatedOpacity( + opacity: _open ? 0.0 : 1.0, + curve: const Interval(0.25, 1.0, curve: Curves.easeInOut), + duration: const Duration(milliseconds: 250), + child: FloatingActionButton( + onPressed: _toggle, + child: const Icon(Icons.create), + ), + ), + ), + ); + } +} + +@immutable +class _ExpandingActionButton extends StatelessWidget { + const _ExpandingActionButton({ + required this.directionInDegrees, + required this.maxDistance, + required this.progress, + required this.child, + }); + + final double directionInDegrees; + final double maxDistance; + final Animation progress; + final Widget child; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: progress, + builder: (context, child) { + final offset = Offset.fromDirection( + directionInDegrees * (math.pi / 180.0), + progress.value * maxDistance, + ); + return Positioned( + right: 4.0 + offset.dx, + bottom: 4.0 + offset.dy, + child: Transform.rotate( + angle: (1.0 - progress.value) * math.pi / 2, + child: child!, + ), + ); + }, + child: FadeTransition( + opacity: progress, + child: child, + ), + ); + } +} + +@immutable +class ActionButton extends StatelessWidget { + const ActionButton({ + super.key, + this.onPressed, + required this.icon, + }); + + final VoidCallback? onPressed; + final Widget icon; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Material( + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + color: theme.colorScheme.secondary, + elevation: 4.0, + child: IconButton( + onPressed: onPressed, + icon: icon, + color: theme.colorScheme.onSecondary, + ), + ); + } +} + +@immutable +class FakeItem extends StatelessWidget { + const FakeItem({ + super.key, + required this.isBig, + }); + + final bool isBig; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0), + height: isBig ? 128.0 : 36.0, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + color: Colors.grey.shade300, + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 112c1dcd4f..c64c50c090 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:example/expandable_floating_action_button.dart'; import 'package:example/plugin/document_node_widget.dart'; import 'package:example/plugin/selected_text_node_widget.dart'; import 'package:example/plugin/text_with_heading_node_widget.dart'; @@ -60,6 +61,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { final RenderPlugins renderPlugins = RenderPlugins(); late EditorState _editorState; + int page = 0; @override void initState() { super.initState(); @@ -80,53 +82,95 @@ class _MyHomePageState extends State { // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), - body: FutureBuilder( - future: rootBundle.loadString('assets/document.json'), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - final data = Map.from(json.decode(snapshot.data!)); - final document = StateTree.fromJson(data); - _editorState = EditorState( - document: document, - renderPlugins: renderPlugins, - ); - return FlowyEditor( - editorState: _editorState, - keyEventHandlers: const [], - shortcuts: [ - // TODO: this won't work, just a example for now. - { - 'h1': (editorState, eventName) { - debugPrint('shortcut => $eventName'); - final selectedNodes = editorState.selectedNodes; - if (selectedNodes.isEmpty) { - return; - } - final textNode = selectedNodes.first as TextNode; - TransactionBuilder(editorState) - ..formatText(textNode, 0, textNode.toRawString().length, { - 'heading': 'h1', - }) - ..commit(); - } - }, - { - 'bold': (editorState, eventName) => - debugPrint('shortcut => $eventName') - }, - { - 'underline': (editorState, eventName) => - debugPrint('shortcut => $eventName') - }, - ], - ); - } - }, + body: _buildBody(), + floatingActionButton: ExpandableFab( + distance: 112.0, + children: [ + ActionButton( + onPressed: () { + if (page == 0) return; + setState(() { + page = 0; + }); + }, + icon: const Icon(Icons.note_add), + ), + ActionButton( + onPressed: () { + if (page == 1) return; + setState(() { + page = 1; + }); + }, + icon: const Icon(Icons.text_fields), + ), + ], ), ); } + + Widget _buildBody() { + if (page == 0) { + return _buildFlowyEditor(); + } else if (page == 1) { + return _buildTextfield(); + } + return Container(); + } + + Widget _buildFlowyEditor() { + return FutureBuilder( + future: rootBundle.loadString('assets/document.json'), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + final data = Map.from(json.decode(snapshot.data!)); + final document = StateTree.fromJson(data); + _editorState = EditorState( + document: document, + renderPlugins: renderPlugins, + ); + return FlowyEditor( + editorState: _editorState, + keyEventHandlers: const [], + shortcuts: [ + // TODO: this won't work, just a example for now. + { + 'h1': (editorState, eventName) { + debugPrint('shortcut => $eventName'); + final selectedNodes = editorState.selectedNodes; + if (selectedNodes.isEmpty) { + return; + } + final textNode = selectedNodes.first as TextNode; + TransactionBuilder(editorState) + ..formatText(textNode, 0, textNode.toRawString().length, { + 'heading': 'h1', + }) + ..commit(); + } + }, + { + 'bold': (editorState, eventName) => + debugPrint('shortcut => $eventName') + }, + { + 'underline': (editorState, eventName) => + debugPrint('shortcut => $eventName') + }, + ], + ); + } + }, + ); + } + + Widget _buildTextfield() { + return const Center( + child: TextField(), + ); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 8b40981ccb..307b586f31 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,4 +1,5 @@ import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; +import 'package:flowy_editor/service/input_service.dart'; import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; @@ -36,22 +37,26 @@ class _FlowyEditorState extends State { return FlowySelection( key: editorState.service.selectionServiceKey, editorState: editorState, - child: FlowyKeyboard( - key: editorState.service.keyboardServiceKey, - handlers: [ - slashShortcutHandler, - flowyDeleteNodesHandler, - deleteSingleTextNodeHandler, - arrowKeysHandler, - ...widget.keyEventHandlers, - ], + child: FlowyInput( + key: editorState.service.inputServiceKey, editorState: editorState, - child: FloatingShortcut( - key: editorState.service.floatingShortcutServiceKey, - size: const Size(200, 150), // TODO: support customize size. + child: FlowyKeyboard( + key: editorState.service.keyboardServiceKey, + handlers: [ + slashShortcutHandler, + flowyDeleteNodesHandler, + deleteSingleTextNodeHandler, + arrowKeysHandler, + ...widget.keyEventHandlers, + ], editorState: editorState, - floatingShortcuts: widget.shortcuts, - child: editorState.build(context), + child: FloatingShortcut( + key: editorState.service.floatingShortcutServiceKey, + size: const Size(200, 150), // TODO: support customize size. + editorState: editorState, + floatingShortcuts: widget.shortcuts, + child: editorState.build(context), + ), ), ), ); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart new file mode 100644 index 0000000000..c02e9828e4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -0,0 +1,179 @@ +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +mixin FlowyInputService { + void attach(TextEditingValue textEditingValue); + void setTextEditingValue(TextEditingValue textEditingValue); + void apply(List deltas); + void close(); +} + +/// process input +class FlowyInput extends StatefulWidget { + const FlowyInput({ + Key? key, + required this.editorState, + required this.child, + }) : super(key: key); + + final EditorState editorState; + final Widget child; + + @override + State createState() => _FlowyInputState(); +} + +class _FlowyInputState extends State + with FlowyInputService + implements DeltaTextInputClient { + TextInputConnection? _textInputConnection; + + EditorState get _editorState => widget.editorState; + + @override + void initState() { + super.initState(); + + _editorState.service.selectionService.currentSelectedNodes + .addListener(_onSelectedNodesChange); + } + + @override + void dispose() { + _editorState.service.selectionService.currentSelectedNodes + .removeListener(_onSelectedNodesChange); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + child: widget.child, + ); + } + + @override + void attach(TextEditingValue textEditingValue) { + if (_textInputConnection != null) { + return; + } + + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + // TODO: customize + enableDeltaModel: true, + inputType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + ), + ); + + _textInputConnection + ?..show() + ..setEditingState(textEditingValue); + } + + @override + void setTextEditingValue(TextEditingValue textEditingValue) { + assert(_textInputConnection != null, + 'Must call `attach` before set textEditingValue'); + if (_textInputConnection != null) { + _textInputConnection?.setEditingState(textEditingValue); + } + } + + @override + void apply(List deltas) {} + + @override + void close() { + _textInputConnection?.close(); + _textInputConnection = null; + } + + @override + void connectionClosed() { + // TODO: implement connectionClosed + } + + @override + // TODO: implement currentAutofillScope + AutofillScope? get currentAutofillScope => throw UnimplementedError(); + + @override + // TODO: implement currentTextEditingValue + TextEditingValue? get currentTextEditingValue => throw UnimplementedError(); + + @override + void insertTextPlaceholder(Size size) { + // TODO: implement insertTextPlaceholder + } + + @override + void performAction(TextInputAction action) { + // TODO: implement performAction + } + + @override + void performPrivateCommand(String action, Map data) { + // TODO: implement performPrivateCommand + } + + @override + void removeTextPlaceholder() { + // TODO: implement removeTextPlaceholder + } + + @override + void showAutocorrectionPromptRect(int start, int end) { + // TODO: implement showAutocorrectionPromptRect + } + + @override + void showToolbar() { + // TODO: implement showToolbar + } + + @override + void updateEditingValue(TextEditingValue value) { + // TODO: implement updateEditingValue + } + + @override + void updateEditingValueWithDeltas(List textEditingDeltas) { + debugPrint(textEditingDeltas.map((delta) => delta.toString()).toString()); + + apply(textEditingDeltas); + } + + @override + void updateFloatingCursor(RawFloatingCursorPoint point) { + // TODO: implement updateFloatingCursor + } + + void _onSelectedNodesChange() { + final nodes = + _editorState.service.selectionService.currentSelectedNodes.value; + final selection = _editorState.service.selectionService.currentSelection; + // FIXME: upward. + if (nodes.isNotEmpty && selection != null) { + final textNodes = nodes.whereType(); + final text = textNodes.fold( + '', (sum, textNode) => '$sum${textNode.toRawString()}\n'); + attach( + TextEditingValue( + text: text, + selection: TextSelection( + baseOffset: selection.start.offset, + extentOffset: selection.end.offset, + ), + ), + ); + } else { + close(); + } + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 19604b0227..be3fde4062 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -17,7 +17,8 @@ mixin FlowySelectionService on State { /// Returns the currently selected [Node]s. /// /// The order of the return is determined according to the selected order. - List get currentSelectedNodes; + ValueNotifier> get currentSelectedNodes; + Selection? get currentSelection; /// ------------------ Selection ------------------------ @@ -112,7 +113,10 @@ class _FlowySelectionState extends State EditorState get editorState => widget.editorState; @override - List currentSelectedNodes = []; + Selection? currentSelection; + + @override + ValueNotifier> currentSelectedNodes = ValueNotifier([]); @override List getNodesInSelection(Selection selection) => @@ -292,7 +296,8 @@ class _FlowySelectionState extends State } void _clearSelection() { - currentSelectedNodes = []; + currentSelection = null; + currentSelectedNodes.value = []; // clear selection _selectionOverlays @@ -312,7 +317,8 @@ class _FlowySelectionState extends State final nodes = _selectedNodesInSelection(editorState.document.root, selection); - currentSelectedNodes = nodes; + currentSelection = selection; + currentSelectedNodes.value = nodes; var index = 0; for (final node in nodes) { @@ -374,7 +380,8 @@ class _FlowySelectionState extends State return; } - currentSelectedNodes = [node]; + currentSelection = Selection.collapsed(position); + currentSelectedNodes.value = [node]; final selectable = node.selectable; final rect = selectable?.getCursorRectInPosition(position); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index f8cf4a9e5c..8fe715bbe7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -14,6 +14,9 @@ class FlowyService { // keyboard service final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service'); + // input service + final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service'); + // floating shortcut service final floatingShortcutServiceKey = GlobalKey(debugLabel: 'flowy_floating_shortcut_service'); From 84eed9e3402f324a4f4ac687c723d35f2dca17da Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 27 Jul 2022 12:16:23 +0800 Subject: [PATCH 068/121] feat: refresh the selection when the metrics changed. --- .../lib/render/node_widget_builder.dart | 24 ++++++------ .../lib/render/selection/cursor_widget.dart | 2 +- .../lib/service/selection_service.dart | 38 +++++++++++++++---- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index a3d35f9dad..06f364d67b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -43,20 +43,20 @@ class NodeWidgetBuilder { 'Node validate failure, node = { type: ${node.type}, attributes: ${node.attributes} }'); } - return _buildNodeChangeNotifier(buildContext); + return _build(buildContext); } - Widget _buildNodeChangeNotifier(BuildContext buildContext) { - return ChangeNotifierProvider.value( - value: node, - builder: (_, __) => Consumer( - builder: ((context, value, child) { - debugPrint('Node changed, and rebuilding...'); - return CompositedTransformTarget( - link: node.layerLink, - child: build(context), - ); - }), + Widget _build(BuildContext buildContext) { + return CompositedTransformTarget( + link: node.layerLink, + child: ChangeNotifierProvider.value( + value: node, + builder: (context, child) => Consumer( + builder: ((context, value, child) { + debugPrint('Node is rebuilding...'); + return build(context); + }), + ), ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart index 2ba42221f0..58d22bec85 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart @@ -11,7 +11,7 @@ class CursorWidget extends StatefulWidget { this.blinkingInterval = 0.5, }) : super(key: key); - final double blinkingInterval; + final double blinkingInterval; // milliseconds final Color color; final Rect rect; final LayerLink layerLink; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index be3fde4062..52c0b84f2c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -96,7 +96,7 @@ class FlowySelection extends StatefulWidget { } class _FlowySelectionState extends State - with FlowySelectionService { + with FlowySelectionService, WidgetsBindingObserver { final _cursorKey = GlobalKey(debugLabel: 'cursor'); final List _selectionOverlays = []; @@ -122,6 +122,28 @@ class _FlowySelectionState extends State List getNodesInSelection(Selection selection) => _selectedNodesInSelection(editorState.document.root, selection); + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + + // Need to refresh the selection when the metrics changed. + if (currentSelection != null) { + updateSelection(currentSelection!); + } + } + + @override + void dispose() { + super.dispose(); + } + @override Widget build(BuildContext context) { return RawGestureDetector( @@ -140,8 +162,8 @@ class _FlowySelectionState extends State TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(), - (recongizer) { - recongizer.onTapDown = _onTapDown; + (recognizer) { + recognizer.onTapDown = _onTapDown; }, ) }, @@ -155,8 +177,10 @@ class _FlowySelectionState extends State // cursor if (selection.isCollapsed) { + debugPrint('Update cursor'); _updateCursor(selection.start); } else { + debugPrint('Update selection'); _updateSelection(selection); } } @@ -171,9 +195,9 @@ class _FlowySelectionState extends State if (end != null) { return computeNodesInRange(editorState.document.root, start, end); } else { - final reuslt = computeNodeInOffset(editorState.document.root, start); - if (reuslt != null) { - return [reuslt]; + final result = computeNodeInOffset(editorState.document.root, start); + if (result != null) { + return [result]; } } return []; @@ -307,7 +331,7 @@ class _FlowySelectionState extends State _cursorOverlays ..forEach((overlay) => overlay.remove()) ..clear(); - // clear floating shortcusts + // clear floating shortcuts editorState.service.floatingShortcutServiceKey.currentState ?.unwrapOrNull() ?.hide(); From c72fead19ca9f93bcbe0f58df32805715d35d41e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 27 Jul 2022 13:27:24 +0800 Subject: [PATCH 069/121] feat: operation transforming --- .../flowy_editor/lib/operation/operation.dart | 85 ++++++++++++++++--- .../flowy_editor/test/operation_test.dart | 49 +++++++++++ 2 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/operation_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index e3710ddb3c..7534b03427 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -1,21 +1,27 @@ -import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/document/attributes.dart'; +import 'package:flowy_editor/flowy_editor.dart'; abstract class Operation { + final Path path; + Operation({required this.path}); + Operation copyWithPath(Path path); Operation invert(); } class InsertOperation extends Operation { - final Path path; final Node value; InsertOperation({ - required this.path, + required super.path, required this.value, }); + InsertOperation copyWith({Path? path, Node? value}) => + InsertOperation(path: path ?? this.path, value: value ?? this.value); + + @override + Operation copyWithPath(Path path) => copyWith(path: path); + @override Operation invert() { return DeleteOperation( @@ -26,16 +32,25 @@ class InsertOperation extends Operation { } class UpdateOperation extends Operation { - final Path path; final Attributes attributes; final Attributes oldAttributes; UpdateOperation({ - required this.path, + required super.path, required this.attributes, required this.oldAttributes, }); + UpdateOperation copyWith( + {Path? path, Attributes? attributes, Attributes? oldAttributes}) => + UpdateOperation( + path: path ?? this.path, + attributes: attributes ?? this.attributes, + oldAttributes: oldAttributes ?? this.oldAttributes); + + @override + Operation copyWithPath(Path path) => copyWith(path: path); + @override Operation invert() { return UpdateOperation( @@ -47,14 +62,19 @@ class UpdateOperation extends Operation { } class DeleteOperation extends Operation { - final Path path; final Node removedValue; DeleteOperation({ - required this.path, + required super.path, required this.removedValue, }); + DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation( + path: path ?? this.path, removedValue: removedValue ?? this.removedValue); + + @override + Operation copyWithPath(Path path) => copyWith(path: path); + @override Operation invert() { return InsertOperation( @@ -65,18 +85,61 @@ class DeleteOperation extends Operation { } class TextEditOperation extends Operation { - final Path path; final Delta delta; final Delta inverted; TextEditOperation({ - required this.path, + required super.path, required this.delta, required this.inverted, }); + TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) => + TextEditOperation( + path: path ?? this.path, + delta: delta ?? this.delta, + inverted: inverted ?? this.inverted); + + @override + Operation copyWithPath(Path path) => copyWith(path: path); + @override Operation invert() { return TextEditOperation(path: path, delta: inverted, inverted: delta); } } + +Path transformPath(Path preInsertPath, Path b, [int delta = 1]) { + if (preInsertPath.length > b.length) { + return b; + } + if (preInsertPath.isEmpty || b.isEmpty) { + return b; + } + // check the prefix + for (var i = 0; i < preInsertPath.length - 1; i++) { + if (preInsertPath[i] != b[i]) { + return b; + } + } + final prefix = preInsertPath.sublist(0, preInsertPath.length - 1); + final suffix = b.sublist(preInsertPath.length); + final preInsertLast = preInsertPath.last; + final bAtIndex = b[preInsertPath.length - 1]; + if (preInsertLast <= bAtIndex) { + prefix.add(bAtIndex + delta); + } + prefix.addAll(suffix); + return prefix; +} + +Operation transformOperation(Operation a, Operation b) { + if (a is InsertOperation) { + final newPath = transformPath(a.path, b.path); + return b.copyWithPath(newPath); + } else if (b is DeleteOperation) { + final newPath = transformPath(a.path, b.path, -1); + return b.copyWithPath(newPath); + } + return b; +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart new file mode 100644 index 0000000000..53c2a243b9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -0,0 +1,49 @@ +import 'dart:collection'; + +import 'package:flowy_editor/document/node.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flowy_editor/operation/operation.dart'; + +void main() { + group('transform path', () { + test('transform path changed', () { + expect(transformPath([0, 1], [0, 1]), [0, 2]); + expect(transformPath([0, 1], [0, 2]), [0, 3]); + expect(transformPath([0, 1], [0, 2, 7, 8, 9]), [0, 3, 7, 8, 9]); + expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]); + }); + test("transform path not changed", () { + expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]); + expect(transformPath([0, 1, 2], [0, 1]), [0, 1]); + }); + test("transform path delta", () { + expect(transformPath([0, 1], [0, 1], 5), [0, 6]); + }); + }); + group('transform operation', () { + test('insert + insert', () { + final t = transformOperation( + InsertOperation(path: [ + 0, + 1 + ], value: Node(type: "node", attributes: {}, children: LinkedList())), + InsertOperation( + path: [0, 1], + value: + Node(type: "node", attributes: {}, children: LinkedList()))); + expect(t.path, [0, 2]); + }); + test('delete + delete', () { + final t = transformOperation( + DeleteOperation( + path: [0, 1], + removedValue: + Node(type: "node", attributes: {}, children: LinkedList())), + DeleteOperation( + path: [0, 2], + removedValue: + Node(type: "node", attributes: {}, children: LinkedList()))); + expect(t.path, [0, 1]); + }); + }); +} From 033410aacd23dd8dd5bc896f722858e46de27ca4 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 27 Jul 2022 13:54:46 +0800 Subject: [PATCH 070/121] feat: transform operation in transaction builder --- .../flowy_editor/lib/document/node.dart | 6 +++- .../flowy_editor/lib/operation/operation.dart | 1 + .../lib/operation/transaction_builder.dart | 3 ++ .../flowy_editor/test/operation_test.dart | 33 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 8b80fd0b51..9871bf24ee 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -31,7 +31,11 @@ class Node extends ChangeNotifier with LinkedListEntry { required this.children, required this.attributes, this.parent, - }); + }) { + for (final child in children) { + child.parent = this; + } + } factory Node.fromJson(Map json) { assert(json['type'] is String); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index 7534b03427..487844af14 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -141,5 +141,6 @@ Operation transformOperation(Operation a, Operation b) { final newPath = transformPath(a.path, b.path, -1); return b.copyWithPath(newPath); } + // TODO: transform update and textedit return b; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 6fca48f230..b7ae2ac878 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -96,6 +96,9 @@ class TransactionBuilder { return; } } + for (var i = 0; i < operations.length; i++) { + op = transformOperation(operations[i], op); + } operations.add(op); } diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index 53c2a243b9..683b6df58e 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -3,6 +3,10 @@ import 'dart:collection'; import 'package:flowy_editor/document/node.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/document/state_tree.dart'; +import 'package:flowy_editor/render/render_plugins.dart'; void main() { group('transform path', () { @@ -46,4 +50,33 @@ void main() { expect(t.path, [0, 1]); }); }); + test('transform transaction builder', () { + final item1 = Node(type: "node", attributes: {}, children: LinkedList()); + final item2 = Node(type: "node", attributes: {}, children: LinkedList()); + final item3 = Node(type: "node", attributes: {}, children: LinkedList()); + final root = Node( + type: "root", + attributes: {}, + children: LinkedList() + ..addAll([ + item1, + item2, + item3, + ])); + final state = EditorState( + document: StateTree(root: root), renderPlugins: RenderPlugins()); + + expect(item1.path, [0]); + expect(item2.path, [1]); + expect(item3.path, [2]); + + final tb = TransactionBuilder(state); + tb.deleteNode(item1); + tb.deleteNode(item2); + tb.deleteNode(item3); + final transaction = tb.finish(); + expect(transaction.operations[0].path, [0]); + expect(transaction.operations[1].path, [0]); + expect(transaction.operations[2].path, [0]); + }); } From 445ff561b56eba912f01d60d966a93633a959340 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 27 Jul 2022 14:43:45 +0800 Subject: [PATCH 071/121] feat: implement handler - press the enter key in the edge of text node --- .../flowy_editor/lib/document/node.dart | 9 ++++ .../lib/extensions/node_extensions.dart | 2 - .../lib/extensions/path_extensions.dart | 11 +++++ .../lib/render/node_widget_builder.dart | 2 +- .../lib/service/editor_service.dart | 2 + .../lib/service/input_service.dart | 16 ++++++- .../enter_in_edge_of_text_node_handler.dart | 46 +++++++++++++++++++ 7 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 8b80fd0b51..9760268e80 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/operation/operation.dart'; import 'package:flutter/material.dart'; import './attributes.dart'; @@ -172,6 +173,14 @@ class TextNode extends Node { required Delta delta, }) : _delta = delta; + TextNode.empty() + : _delta = Delta([TextInsert('')]), + super( + type: 'text', + children: LinkedList(), + attributes: {}, + ); + Delta get delta { return _delta; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart index 49cc38f749..52b7596240 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart index b37d846482..793dc552dd 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart @@ -22,4 +22,15 @@ extension PathExtensions on Path { } return true; } + + Path get next { + Path nextPath = Path.from(this, growable: true); + if (isEmpty) { + return nextPath; + } + final last = nextPath.last; + return nextPath + ..removeLast() + ..add(last + 1); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index 06f364d67b..659c380720 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -31,7 +31,7 @@ class NodeWidgetBuilder { throw UnimplementedError(); /// TODO: refactore this part. - /// return widget embeded with ChangeNotifier and widget itself. + /// return widget embedded with ChangeNotifier and widget itself. Widget call( BuildContext buildContext, ) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 307b586f31..d5223ec36a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,5 +1,6 @@ import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; import 'package:flowy_editor/service/input_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; @@ -47,6 +48,7 @@ class _FlowyEditorState extends State { flowyDeleteNodesHandler, deleteSingleTextNodeHandler, arrowKeysHandler, + enterInEdgeOfTextNodeHandler, ...widget.keyEventHandlers, ], editorState: editorState, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart index c02e9828e4..bdbcd24467 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -1,5 +1,8 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -86,7 +89,18 @@ class _FlowyInputState extends State } @override - void apply(List deltas) {} + void apply(List deltas) { +// TODO: implement the detail + for (final delta in deltas) { + if (delta is TextEditingDeltaInsertion) { + } else if (delta is TextEditingDeltaDeletion) { + } else if (delta is TextEditingDeltaReplacement) { + } else if (delta is TextEditingDeltaNonTextUpdate) { + // We don't need to care the [TextEditingDeltaNonTextUpdate]. + // Do nothing. + } + } + } @override void close() { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart new file mode 100644 index 0000000000..d1e89d393e --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart @@ -0,0 +1,46 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/extensions/path_extensions.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.enter) { + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final selection = editorState.service.selectionService.currentSelection; + if (selection == null || + nodes.length != 1 || + nodes.first is! TextNode || + !selection.isCollapsed) { + return KeyEventResult.ignored; + } + + final textNode = nodes.first as TextNode; + + if (textNode.selectable!.end() == selection.end) { + TransactionBuilder(editorState) + ..insertNode( + textNode.path.next, + TextNode.empty(), + ) + ..commit(); + return KeyEventResult.handled; + } else if (textNode.selectable!.start() == selection.start) { + TransactionBuilder(editorState) + ..insertNode( + textNode.path, + TextNode.empty(), + ) + ..commit(); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; +}; From e74f5e84dc0db1293f4a478988a16e589798d0a1 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 27 Jul 2022 15:46:43 +0800 Subject: [PATCH 072/121] feat: handle arrow keys --- .../example/lib/plugin/text_node_widget.dart | 2 +- .../flowy_editor/lib/editor_state.dart | 19 +++++- .../arrow_keys_handler.dart | 59 +++++++++++++++++++ .../lib/service/selection_service.dart | 21 +++---- 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index a67ebcd2ad..53c33cd295 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -126,7 +126,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> textCapitalization: TextCapitalization.sentences, ), ); - editorState.cursorSelection = _localSelectionToGlobal(node, selection); + editorState.updateCursorSelection(_localSelectionToGlobal(node, selection)); _textInputConnection ?..show() ..setEditingState( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index cd503843c2..a69b053c90 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -31,7 +31,22 @@ class EditorState { final service = FlowyService(); final UndoManager undoManager = UndoManager(); - Selection? cursorSelection; + Selection? _cursorSelection; + + Selection? get cursorSelection { + return _cursorSelection; + } + + /// add the set reason in the future, don't use setter + updateCursorSelection(Selection? cursorSelection) { + // broadcast to other users here + if (cursorSelection == null) { + service.selectionService.clearSelection(); + } else { + service.selectionService.updateSelection(cursorSelection); + } + _cursorSelection = cursorSelection; + } Timer? _debouncedSealHistoryItemTimer; @@ -58,7 +73,7 @@ class EditorState { for (final op in transaction.operations) { _applyOperation(op); } - cursorSelection = transaction.afterSelection; + updateCursorSelection(transaction.afterSelection); if (options.recordUndo) { final undoItem = undoManager.getUndoHistoryItem(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 95496db2ea..bdb473042d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,7 +1,17 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +int _endOffsetOfNode(Node node) { + if (node is TextNode) { + return node.delta.length; + } + return 0; +} + FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.arrowUp && event.logicalKey != LogicalKeyboardKey.arrowDown && @@ -10,5 +20,54 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { return KeyEventResult.ignored; } + final currentSelection = editorState.cursorSelection; + if (currentSelection == null) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + // turn left + if (currentSelection.isCollapsed) { + final end = currentSelection.end; + final offset = end.offset; + if (offset == 0) { + final node = editorState.document.nodeAtPath(end.path)!; + final prevNode = node.previous; + if (prevNode != null) { + editorState.updateCursorSelection(Selection.collapsed(Position( + path: prevNode.path, offset: _endOffsetOfNode(prevNode)))); + } + return KeyEventResult.handled; + } + editorState.updateCursorSelection( + Selection.collapsed(Position(path: end.path, offset: offset - 1))); + } else { + editorState + .updateCursorSelection(currentSelection.collapse(atStart: true)); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + if (currentSelection.isCollapsed) { + final end = currentSelection.end; + final offset = end.offset; + final node = editorState.document.nodeAtPath(end.path)!; + final lengthOfNode = _endOffsetOfNode(node); + if (offset >= lengthOfNode) { + final nextNode = node.next; + if (nextNode != null) { + editorState.updateCursorSelection( + Selection.collapsed(Position(path: nextNode.path, offset: 0))); + } + return KeyEventResult.handled; + } + + editorState.updateCursorSelection( + Selection.collapsed(Position(path: end.path, offset: offset + 1))); + } else { + editorState.updateCursorSelection(currentSelection.collapse()); + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; }; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 19604b0227..e3c262a360 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,4 +1,3 @@ -import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; @@ -49,7 +48,7 @@ mixin FlowySelectionService on State { /// [start] is the offset under the global coordinate system. Node? computeNodeInOffset(Node node, Offset offset); - /// Return the [Node]s in multiple selection. Emtpy list would be returned + /// Return the [Node]s in multiple selection. Empty list would be returned /// if no nodes are in range. /// /// [start] is the offset under the global coordinate system. @@ -136,8 +135,8 @@ class _FlowySelectionState extends State TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(), - (recongizer) { - recongizer.onTapDown = _onTapDown; + (recognizer) { + recognizer.onTapDown = _onTapDown; }, ) }, @@ -167,9 +166,9 @@ class _FlowySelectionState extends State if (end != null) { return computeNodesInRange(editorState.document.root, start, end); } else { - final reuslt = computeNodeInOffset(editorState.document.root, start); - if (reuslt != null) { - return [reuslt]; + final result = computeNodeInOffset(editorState.document.root, start); + if (result != null) { + return [result]; } } return []; @@ -253,8 +252,10 @@ class _FlowySelectionState extends State if (selectable != null) { final position = selectable.getPositionInOffset(tapOffset!); final selection = Selection.collapsed(position); - updateSelection(selection); + editorState.updateCursorSelection(selection); } + } else { + editorState.updateCursorSelection(null); } } @@ -283,7 +284,7 @@ class _FlowySelectionState extends State final selection = Selection( start: isDownward ? start : end, end: isDownward ? end : start); debugPrint('[_onPanUpdate] $selection'); - updateSelection(selection); + editorState.updateCursorSelection(selection); } } @@ -302,7 +303,7 @@ class _FlowySelectionState extends State _cursorOverlays ..forEach((overlay) => overlay.remove()) ..clear(); - // clear floating shortcusts + // clear floating shortcuts editorState.service.floatingShortcutServiceKey.currentState ?.unwrapOrNull() ?.hide(); From 53b982e7c9b50a26612e80d7ab44c444cc465277 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 27 Jul 2022 15:58:25 +0800 Subject: [PATCH 073/121] feat: arrow up and down --- .../example/lib/plugin/image_node_widget.dart | 5 ++ .../lib/plugin/selected_text_node_widget.dart | 5 ++ .../lib/render/selection/cursor_widget.dart | 25 +++++++-- .../lib/render/selection/selectable.dart | 2 + .../arrow_keys_handler.dart | 22 +++++++- .../lib/service/selection_service.dart | 56 +++++++++++++++---- 6 files changed, 98 insertions(+), 17 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index c5084df2fb..00a7fce8ad 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -63,6 +63,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { throw UnimplementedError(); } + @override + Offset localToGlobal(Offset offset) { + throw UnimplementedError(); + } + @override Rect getCursorRectInPosition(Position position) { // TODO: implement getCursorRectInPosition diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 894f6b1848..3238decb81 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -70,6 +70,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> ); } + @override + Offset localToGlobal(Offset offset) { + return _renderParagraph.localToGlobal(offset); + } + @override List getRectsInSelection(Selection selection) { assert(pathEquals(selection.start.path, selection.end.path)); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart index 2ba42221f0..3e11073729 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart @@ -17,10 +17,10 @@ class CursorWidget extends StatefulWidget { final LayerLink layerLink; @override - State createState() => _CursorWidgetState(); + State createState() => CursorWidgetState(); } -class _CursorWidgetState extends State { +class CursorWidgetState extends State { bool showCursor = true; late Timer timer; @@ -28,7 +28,17 @@ class _CursorWidgetState extends State { void initState() { super.initState(); - timer = Timer.periodic( + timer = _initTimer(); + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + + Timer _initTimer() { + return Timer.periodic( Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()), (timer) { setState(() { @@ -37,10 +47,13 @@ class _CursorWidgetState extends State { }); } - @override - void dispose() { + /// force the cursor widget to show for a while + show() { + setState(() { + showCursor = true; + }); timer.cancel(); - super.dispose(); + timer = _initTimer(); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 4d155972df..df5649e320 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -23,6 +23,8 @@ mixin Selectable on State { Position getPositionInOffset(Offset start); Rect getCursorRectInPosition(Position position); + Offset localToGlobal(Offset offset); + Position start(); Position end(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index bdb473042d..30b295765e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -2,6 +2,7 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -26,7 +27,6 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { } if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - // turn left if (currentSelection.isCollapsed) { final end = currentSelection.end; final offset = end.offset; @@ -67,6 +67,26 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { editorState.updateCursorSelection(currentSelection.collapse()); } return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return KeyEventResult.handled; + } + final first = rects.first; + final firstOffset = Offset(first.left, first.top); + final hitOffset = firstOffset - Offset(0, first.height * 0.5); + editorState.service.selectionService.hit(hitOffset); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return KeyEventResult.handled; + } + final first = rects.last; + final firstOffset = Offset(first.right, first.bottom); + final hitOffset = firstOffset + Offset(0, first.height * 0.5); + editorState.service.selectionService.hit(hitOffset); + return KeyEventResult.handled; } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index e3c262a360..fee31ab4c0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,6 +1,7 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/render/selection/cursor_widget.dart'; import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; @@ -26,6 +27,10 @@ mixin FlowySelectionService on State { /// void clearSelection(); + List rects(); + + hit(Offset? offset); + /// List getNodesInSelection(Selection selection); @@ -108,6 +113,8 @@ class _FlowySelectionState extends State /// Tap Offset? tapOffset; + final List _rects = []; + EditorState get editorState => widget.editorState; @override @@ -144,8 +151,13 @@ class _FlowySelectionState extends State ); } + List rects() { + return _rects; + } + @override void updateSelection(Selection selection) { + _rects.clear(); _clearSelection(); // cursor @@ -245,18 +257,29 @@ class _FlowySelectionState extends State tapOffset = details.globalPosition; - final nodes = getNodesInRange(tapOffset!); - if (nodes.isNotEmpty) { - assert(nodes.length == 1); - final selectable = nodes.first.selectable; - if (selectable != null) { - final position = selectable.getPositionInOffset(tapOffset!); - final selection = Selection.collapsed(position); - editorState.updateCursorSelection(selection); - } - } else { + hit(tapOffset); + } + + @override + hit(Offset? offset) { + if (offset == null) { editorState.updateCursorSelection(null); + return; } + final nodes = getNodesInRange(offset); + if (nodes.isEmpty) { + editorState.updateCursorSelection(null); + return; + } + assert(nodes.length == 1); + final selectable = nodes.first.selectable; + if (selectable == null) { + editorState.updateCursorSelection(null); + return; + } + final position = selectable.getPositionInOffset(offset); + final selection = Selection.collapsed(position); + editorState.updateCursorSelection(selection); } void _onPanStart(DragStartDetails details) { @@ -353,6 +376,7 @@ class _FlowySelectionState extends State final rects = selectable.getRectsInSelection(newSelection); for (final rect in rects) { + _rects.add(_transformRectToGlobal(selectable, rect)); final overlay = OverlayEntry( builder: ((context) => SelectionWidget( color: widget.selectionColor, @@ -367,6 +391,11 @@ class _FlowySelectionState extends State Overlay.of(context)?.insertAll(_selectionOverlays); } + Rect _transformRectToGlobal(Selectable selectable, Rect r) { + final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top)); + return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height); + } + void _updateCursor(Position position) { final node = editorState.document.root.childAtPath(position.path); @@ -380,6 +409,7 @@ class _FlowySelectionState extends State final selectable = node.selectable; final rect = selectable?.getCursorRectInPosition(position); if (rect != null) { + _rects.add(_transformRectToGlobal(selectable!, rect)); final cursor = OverlayEntry( builder: ((context) => CursorWidget( key: _cursorKey, @@ -390,9 +420,15 @@ class _FlowySelectionState extends State ); _cursorOverlays.add(cursor); Overlay.of(context)?.insertAll(_cursorOverlays); + _forceShowCursor(); } } + _forceShowCursor() { + final currentState = _cursorKey.currentState as CursorWidgetState?; + currentState?.show(); + } + List _selectedNodesInSelection(Node node, Selection selection) { List result = []; if (node.parent != null) { From 9851b26f2250a7b9e8f4f0835902beaeaa31319a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 27 Jul 2022 19:42:09 +0800 Subject: [PATCH 074/121] fix: transform error for path --- .../packages/flowy_editor/lib/operation/operation.dart | 2 ++ .../app_flowy/packages/flowy_editor/test/operation_test.dart | 1 + 2 files changed, 3 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index 487844af14..eafa4a31da 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -128,6 +128,8 @@ Path transformPath(Path preInsertPath, Path b, [int delta = 1]) { final bAtIndex = b[preInsertPath.length - 1]; if (preInsertLast <= bAtIndex) { prefix.add(bAtIndex + delta); + } else { + prefix.add(bAtIndex); } prefix.addAll(suffix); return prefix; diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index 683b6df58e..176f00b734 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -19,6 +19,7 @@ void main() { test("transform path not changed", () { expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]); expect(transformPath([0, 1, 2], [0, 1]), [0, 1]); + expect(transformPath([1, 1], [1, 0]), [1, 0]); }); test("transform path delta", () { expect(transformPath([0, 1], [0, 1], 5), [0, 6]); From 45a8566e61f44d2705d0e30edbe6e555f39cf02d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 27 Jul 2022 20:24:26 +0800 Subject: [PATCH 075/121] feat: implement rich text component in flowy_ediotr and support markdown style rendering. --- .../flowy_editor/example/assets/document.json | 21 +- .../flowy_editor/example/lib/main.dart | 3 +- .../lib/plugin/document_node_widget.dart | 2 +- .../example/lib/plugin/image_node_widget.dart | 2 +- .../lib/plugin/old_text_node_widget.dart | 352 ++++++++++++++++++ .../lib/plugin/selected_text_node_widget.dart | 19 +- .../example/lib/plugin/text_node_widget.dart | 352 ------------------ .../text_with_check_box_node_widget.dart | 4 +- .../plugin/text_with_heading_node_widget.dart | 4 +- .../flowy_editor/lib/editor_state.dart | 4 + .../flowy_editor/lib/flowy_editor.dart | 2 + .../lib/render/node_widget_builder.dart | 8 +- .../lib/render/rich_text/flowy_rich_text.dart | 215 +++++++++++ .../lib/render/rich_text/rich_text_style.dart | 182 +++++++++ .../lib/render/selection/selectable.dart | 11 +- .../delete_single_text_node_handler.dart | 106 +++--- .../shortcut_handler.dart | 18 - .../lib/service/selection_service.dart | 3 + 18 files changed, 853 insertions(+), 455 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 00ef06da5d..b90aec8369 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -37,7 +37,22 @@ "type": "text", "delta": [{ "insert": "Click anywhere and just start typing." }], "attributes": { - "checkbox": true + "list": "todo", + "todo": false + } + }, + { + "type": "text", + "delta": [{ "insert": "Click anywhere and just start typing." }], + "attributes": { + "list": "bullet" + } + }, + { + "type": "text", + "delta": [{ "insert": "Click anywhere and just start typing." }], + "attributes": { + "list": "bullet" } }, { @@ -77,7 +92,9 @@ "insert": "1. Click the '?' at the bottom right for help and support." } ], - "attributes": {} + "attributes": { + "quotes": true + } }, { "type": "text", diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index c64c50c090..b8b836cc4d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -5,7 +5,7 @@ import 'package:example/plugin/document_node_widget.dart'; import 'package:example/plugin/selected_text_node_widget.dart'; import 'package:example/plugin/text_with_heading_node_widget.dart'; import 'package:example/plugin/image_node_widget.dart'; -import 'package:example/plugin/text_node_widget.dart'; +import 'package:example/plugin/old_text_node_widget.dart'; import 'package:example/plugin/text_with_check_box_node_widget.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; @@ -68,7 +68,6 @@ class _MyHomePageState extends State { renderPlugins ..register('editor', EditorNodeWidgetBuilder.create) - ..register('text', SelectedTextNodeBuilder.create) ..register('image', ImageNodeBuilder.create) ..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create) ..register('text/with-heading', TextWithHeadingNodeBuilder.create); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart index 2db1ef89c4..2a70da2ba2 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart @@ -9,7 +9,7 @@ class EditorNodeWidgetBuilder extends NodeWidgetBuilder { }) : super.create(); @override - Widget build(BuildContext buildContext) { + Widget build(BuildContext context) { return SingleChildScrollView( key: key, child: _EditorNodeWidget( diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index c5084df2fb..aaca3148c2 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -11,7 +11,7 @@ class ImageNodeBuilder extends NodeWidgetBuilder { }) : super.create(); @override - Widget build(BuildContext buildContext) { + Widget build(BuildContext context) { return _ImageNodeWidget( key: key, node: node, diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart new file mode 100644 index 0000000000..bad07fe6a6 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart @@ -0,0 +1,352 @@ +// import 'package:flowy_editor/document/position.dart'; +// import 'package:flowy_editor/document/selection.dart'; +// import 'package:flutter/gestures.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flowy_editor/flowy_editor.dart'; +// import 'package:flutter/services.dart'; +// import 'package:url_launcher/url_launcher_string.dart'; +// import 'flowy_selectable_text.dart'; + +// class TextNodeBuilder extends NodeWidgetBuilder { +// TextNodeBuilder.create({ +// required super.node, +// required super.editorState, +// required super.key, +// }) : super.create() { +// nodeValidator = ((node) { +// return node.type == 'text'; +// }); +// } + +// @override +// Widget build(BuildContext context) { +// return _TextNodeWidget(key: key, node: node, editorState: editorState); +// } +// } + +// class _TextNodeWidget extends StatefulWidget { +// final Node node; +// final EditorState editorState; + +// const _TextNodeWidget({ +// Key? key, +// required this.node, +// required this.editorState, +// }) : super(key: key); + +// @override +// State<_TextNodeWidget> createState() => __TextNodeWidgetState(); +// } + +// class __TextNodeWidgetState extends State<_TextNodeWidget> +// implements DeltaTextInputClient { +// TextNode get node => widget.node as TextNode; +// EditorState get editorState => widget.editorState; +// bool _metaKeyDown = false; +// bool _shiftKeyDown = false; + +// TextInputConnection? _textInputConnection; + +// @override +// Widget build(BuildContext context) { +// return Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// FlowySelectableText.rich( +// node.toTextSpan(), +// showCursor: true, +// enableInteractiveSelection: true, +// onSelectionChanged: _onSelectionChanged, +// // autofocus: true, +// focusNode: FocusNode( +// onKey: _onKey, +// ), +// ), +// if (node.children.isNotEmpty) +// ...node.children.map( +// (e) => editorState.renderPlugins.buildWidget( +// context: NodeWidgetContext( +// buildContext: context, +// node: e, +// editorState: editorState, +// ), +// ), +// ), +// const SizedBox( +// height: 10, +// ), +// ], +// ); +// } + +// KeyEventResult _onKey(FocusNode focusNode, RawKeyEvent event) { +// debugPrint('key: $event'); +// if (event is RawKeyDownEvent) { +// final sel = _globalSelectionToLocal(node, editorState.cursorSelection); +// if (event.logicalKey == LogicalKeyboardKey.backspace) { +// _backDeleteTextAtSelection(sel); +// return KeyEventResult.handled; +// } else if (event.logicalKey == LogicalKeyboardKey.delete) { +// _forwardDeleteTextAtSelection(sel); +// return KeyEventResult.handled; +// } else if (event.logicalKey == LogicalKeyboardKey.metaLeft || +// event.logicalKey == LogicalKeyboardKey.metaRight) { +// _metaKeyDown = true; +// } else if (event.logicalKey == LogicalKeyboardKey.shiftLeft || +// event.logicalKey == LogicalKeyboardKey.shiftRight) { +// _shiftKeyDown = true; +// } else if (event.logicalKey == LogicalKeyboardKey.keyZ && _metaKeyDown) { +// if (_shiftKeyDown) { +// editorState.undoManager.redo(); +// } else { +// editorState.undoManager.undo(); +// } +// } +// } else if (event is RawKeyUpEvent) { +// if (event.logicalKey == LogicalKeyboardKey.metaLeft || +// event.logicalKey == LogicalKeyboardKey.metaRight) { +// _metaKeyDown = false; +// } +// if (event.logicalKey == LogicalKeyboardKey.shiftLeft || +// event.logicalKey == LogicalKeyboardKey.shiftRight) { +// _shiftKeyDown = false; +// } +// } +// return KeyEventResult.ignored; +// } + +// void _onSelectionChanged( +// TextSelection selection, SelectionChangedCause? cause) { +// _textInputConnection?.close(); +// _textInputConnection = TextInput.attach( +// this, +// const TextInputConfiguration( +// enableDeltaModel: true, +// inputType: TextInputType.multiline, +// textCapitalization: TextCapitalization.sentences, +// ), +// ); +// editorState.cursorSelection = _localSelectionToGlobal(node, selection); +// _textInputConnection +// ?..show() +// ..setEditingState( +// TextEditingValue( +// text: node.toRawString(), +// selection: selection, +// ), +// ); +// } + +// _backDeleteTextAtSelection(TextSelection? sel) { +// if (sel == null) { +// return; +// } +// if (sel.start == 0) { +// return; +// } + +// if (sel.isCollapsed) { +// TransactionBuilder(editorState) +// ..deleteText(node, sel.start - 1, 1) +// ..commit(); +// } else { +// TransactionBuilder(editorState) +// ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset) +// ..commit(); +// } + +// _setEditingStateFromGlobal(); +// } + +// _forwardDeleteTextAtSelection(TextSelection? sel) { +// if (sel == null) { +// return; +// } + +// if (sel.isCollapsed) { +// TransactionBuilder(editorState) +// ..deleteText(node, sel.start, 1) +// ..commit(); +// } else { +// TransactionBuilder(editorState) +// ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset) +// ..commit(); +// } +// _setEditingStateFromGlobal(); +// } + +// _setEditingStateFromGlobal() { +// _textInputConnection?.setEditingState(TextEditingValue( +// text: node.toRawString(), +// selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? +// const TextSelection.collapsed(offset: 0))); +// } + +// @override +// void connectionClosed() { +// // TODO: implement connectionClosed +// } + +// @override +// // TODO: implement currentAutofillScope +// AutofillScope? get currentAutofillScope => throw UnimplementedError(); + +// @override +// // TODO: implement currentTextEditingValue +// TextEditingValue? get currentTextEditingValue => TextEditingValue( +// text: node.toRawString(), +// selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? +// const TextSelection.collapsed(offset: 0)); + +// @override +// void insertTextPlaceholder(Size size) { +// // TODO: implement insertTextPlaceholder +// } + +// @override +// void performAction(TextInputAction action) {} + +// @override +// void performPrivateCommand(String action, Map data) { +// // TODO: implement performPrivateCommand +// } + +// @override +// void removeTextPlaceholder() { +// // TODO: implement removeTextPlaceholder +// } + +// @override +// void showAutocorrectionPromptRect(int start, int end) { +// // TODO: implement showAutocorrectionPromptRect +// } + +// @override +// void showToolbar() { +// // TODO: implement showToolbar +// } + +// @override +// void updateEditingValue(TextEditingValue value) {} + +// @override +// void updateEditingValueWithDeltas(List textEditingDeltas) { +// for (final textDelta in textEditingDeltas) { +// if (textDelta is TextEditingDeltaInsertion) { +// TransactionBuilder(editorState) +// ..insertText(node, textDelta.insertionOffset, textDelta.textInserted) +// ..commit(); +// } else if (textDelta is TextEditingDeltaDeletion) { +// TransactionBuilder(editorState) +// ..deleteText(node, textDelta.deletedRange.start, +// textDelta.deletedRange.end - textDelta.deletedRange.start) +// ..commit(); +// } +// } +// } + +// @override +// void updateFloatingCursor(RawFloatingCursorPoint point) { +// // TODO: implement updateFloatingCursor +// } +// } + +// extension on TextNode { +// TextSpan toTextSpan() => TextSpan( +// children: delta.operations +// .whereType() +// .map((op) => op.toTextSpan()) +// .toList()); +// } + +// extension on TextInsert { +// TextSpan toTextSpan() { +// FontWeight? fontWeight; +// FontStyle? fontStyle; +// TextDecoration? decoration; +// GestureRecognizer? gestureRecognizer; +// Color? color; +// Color highLightColor = Colors.transparent; +// double fontSize = 16.0; +// final attributes = this.attributes; +// if (attributes?['bold'] == true) { +// fontWeight = FontWeight.bold; +// } +// if (attributes?['italic'] == true) { +// fontStyle = FontStyle.italic; +// } +// if (attributes?['underline'] == true) { +// decoration = TextDecoration.underline; +// } +// if (attributes?['strikethrough'] == true) { +// decoration = TextDecoration.lineThrough; +// } +// if (attributes?['highlight'] is String) { +// highLightColor = Color(int.parse(attributes!['highlight'])); +// } +// if (attributes?['href'] is String) { +// color = const Color.fromARGB(255, 55, 120, 245); +// decoration = TextDecoration.underline; +// gestureRecognizer = TapGestureRecognizer() +// ..onTap = () { +// launchUrlString(attributes?['href']); +// }; +// } +// final heading = attributes?['heading'] as String?; +// if (heading != null) { +// // TODO: make it better +// if (heading == 'h1') { +// fontSize = 30.0; +// } else if (heading == 'h2') { +// fontSize = 20.0; +// } +// fontWeight = FontWeight.bold; +// } +// return TextSpan( +// text: content, +// style: TextStyle( +// fontWeight: fontWeight, +// fontStyle: fontStyle, +// decoration: decoration, +// color: color, +// fontSize: fontSize, +// backgroundColor: highLightColor, +// ), +// recognizer: gestureRecognizer, +// ); +// } +// } + +// TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) { +// if (globalSel == null) { +// return null; +// } +// final nodePath = node.path; + +// if (!pathEquals(nodePath, globalSel.start.path)) { +// return null; +// } +// if (globalSel.isCollapsed) { +// return TextSelection( +// baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset); +// } else { +// if (pathEquals(globalSel.start.path, globalSel.end.path)) { +// return TextSelection( +// baseOffset: globalSel.start.offset, +// extentOffset: globalSel.end.offset); +// } +// } +// return null; +// } + +// Selection? _localSelectionToGlobal(Node node, TextSelection? sel) { +// if (sel == null) { +// return null; +// } +// final nodePath = node.path; + +// return Selection( +// start: Position(path: nodePath, offset: sel.baseOffset), +// end: Position(path: nodePath, offset: sel.extentOffset), +// ); +// } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 894f6b1848..e16abaa1aa 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -22,7 +22,7 @@ class SelectedTextNodeBuilder extends NodeWidgetBuilder { } @override - Widget build(BuildContext buildContext) { + Widget build(BuildContext context) { return _SelectedTextNodeWidget( key: key, node: node, @@ -96,14 +96,15 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - TextSelection? getCurrentTextSelection() { - return _textSelection; - } - - @override - Offset getOffsetByTextSelection(TextSelection textSelection) { - final offset = _computeCursorRect(textSelection.baseOffset).center; - return _renderParagraph.localToGlobal(offset); + TextSelection? getTextSelectionInSelection(Selection selection) { + assert(selection.isCollapsed); + if (!selection.isCollapsed) { + return null; + } + return TextSelection( + baseOffset: selection.start.offset, + extentOffset: selection.end.offset, + ); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart deleted file mode 100644 index a67ebcd2ad..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ /dev/null @@ -1,352 +0,0 @@ -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/services.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'flowy_selectable_text.dart'; - -class TextNodeBuilder extends NodeWidgetBuilder { - TextNodeBuilder.create({ - required super.node, - required super.editorState, - required super.key, - }) : super.create() { - nodeValidator = ((node) { - return node.type == 'text'; - }); - } - - @override - Widget build(BuildContext buildContext) { - return _TextNodeWidget(key: key, node: node, editorState: editorState); - } -} - -class _TextNodeWidget extends StatefulWidget { - final Node node; - final EditorState editorState; - - const _TextNodeWidget({ - Key? key, - required this.node, - required this.editorState, - }) : super(key: key); - - @override - State<_TextNodeWidget> createState() => __TextNodeWidgetState(); -} - -class __TextNodeWidgetState extends State<_TextNodeWidget> - implements DeltaTextInputClient { - TextNode get node => widget.node as TextNode; - EditorState get editorState => widget.editorState; - bool _metaKeyDown = false; - bool _shiftKeyDown = false; - - TextInputConnection? _textInputConnection; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySelectableText.rich( - node.toTextSpan(), - showCursor: true, - enableInteractiveSelection: true, - onSelectionChanged: _onSelectionChanged, - // autofocus: true, - focusNode: FocusNode( - onKey: _onKey, - ), - ), - if (node.children.isNotEmpty) - ...node.children.map( - (e) => editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: e, - editorState: editorState, - ), - ), - ), - const SizedBox( - height: 10, - ), - ], - ); - } - - KeyEventResult _onKey(FocusNode focusNode, RawKeyEvent event) { - debugPrint('key: $event'); - if (event is RawKeyDownEvent) { - final sel = _globalSelectionToLocal(node, editorState.cursorSelection); - if (event.logicalKey == LogicalKeyboardKey.backspace) { - _backDeleteTextAtSelection(sel); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.delete) { - _forwardDeleteTextAtSelection(sel); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.metaLeft || - event.logicalKey == LogicalKeyboardKey.metaRight) { - _metaKeyDown = true; - } else if (event.logicalKey == LogicalKeyboardKey.shiftLeft || - event.logicalKey == LogicalKeyboardKey.shiftRight) { - _shiftKeyDown = true; - } else if (event.logicalKey == LogicalKeyboardKey.keyZ && _metaKeyDown) { - if (_shiftKeyDown) { - editorState.undoManager.redo(); - } else { - editorState.undoManager.undo(); - } - } - } else if (event is RawKeyUpEvent) { - if (event.logicalKey == LogicalKeyboardKey.metaLeft || - event.logicalKey == LogicalKeyboardKey.metaRight) { - _metaKeyDown = false; - } - if (event.logicalKey == LogicalKeyboardKey.shiftLeft || - event.logicalKey == LogicalKeyboardKey.shiftRight) { - _shiftKeyDown = false; - } - } - return KeyEventResult.ignored; - } - - void _onSelectionChanged( - TextSelection selection, SelectionChangedCause? cause) { - _textInputConnection?.close(); - _textInputConnection = TextInput.attach( - this, - const TextInputConfiguration( - enableDeltaModel: true, - inputType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - ), - ); - editorState.cursorSelection = _localSelectionToGlobal(node, selection); - _textInputConnection - ?..show() - ..setEditingState( - TextEditingValue( - text: node.toRawString(), - selection: selection, - ), - ); - } - - _backDeleteTextAtSelection(TextSelection? sel) { - if (sel == null) { - return; - } - if (sel.start == 0) { - return; - } - - if (sel.isCollapsed) { - TransactionBuilder(editorState) - ..deleteText(node, sel.start - 1, 1) - ..commit(); - } else { - TransactionBuilder(editorState) - ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset) - ..commit(); - } - - _setEditingStateFromGlobal(); - } - - _forwardDeleteTextAtSelection(TextSelection? sel) { - if (sel == null) { - return; - } - - if (sel.isCollapsed) { - TransactionBuilder(editorState) - ..deleteText(node, sel.start, 1) - ..commit(); - } else { - TransactionBuilder(editorState) - ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset) - ..commit(); - } - _setEditingStateFromGlobal(); - } - - _setEditingStateFromGlobal() { - _textInputConnection?.setEditingState(TextEditingValue( - text: node.toRawString(), - selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? - const TextSelection.collapsed(offset: 0))); - } - - @override - void connectionClosed() { - // TODO: implement connectionClosed - } - - @override - // TODO: implement currentAutofillScope - AutofillScope? get currentAutofillScope => throw UnimplementedError(); - - @override - // TODO: implement currentTextEditingValue - TextEditingValue? get currentTextEditingValue => TextEditingValue( - text: node.toRawString(), - selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? - const TextSelection.collapsed(offset: 0)); - - @override - void insertTextPlaceholder(Size size) { - // TODO: implement insertTextPlaceholder - } - - @override - void performAction(TextInputAction action) {} - - @override - void performPrivateCommand(String action, Map data) { - // TODO: implement performPrivateCommand - } - - @override - void removeTextPlaceholder() { - // TODO: implement removeTextPlaceholder - } - - @override - void showAutocorrectionPromptRect(int start, int end) { - // TODO: implement showAutocorrectionPromptRect - } - - @override - void showToolbar() { - // TODO: implement showToolbar - } - - @override - void updateEditingValue(TextEditingValue value) {} - - @override - void updateEditingValueWithDeltas(List textEditingDeltas) { - for (final textDelta in textEditingDeltas) { - if (textDelta is TextEditingDeltaInsertion) { - TransactionBuilder(editorState) - ..insertText(node, textDelta.insertionOffset, textDelta.textInserted) - ..commit(); - } else if (textDelta is TextEditingDeltaDeletion) { - TransactionBuilder(editorState) - ..deleteText(node, textDelta.deletedRange.start, - textDelta.deletedRange.end - textDelta.deletedRange.start) - ..commit(); - } - } - } - - @override - void updateFloatingCursor(RawFloatingCursorPoint point) { - // TODO: implement updateFloatingCursor - } -} - -extension on TextNode { - TextSpan toTextSpan() => TextSpan( - children: delta.operations - .whereType() - .map((op) => op.toTextSpan()) - .toList()); -} - -extension on TextInsert { - TextSpan toTextSpan() { - FontWeight? fontWeight; - FontStyle? fontStyle; - TextDecoration? decoration; - GestureRecognizer? gestureRecognizer; - Color? color; - Color highLightColor = Colors.transparent; - double fontSize = 16.0; - final attributes = this.attributes; - if (attributes?['bold'] == true) { - fontWeight = FontWeight.bold; - } - if (attributes?['italic'] == true) { - fontStyle = FontStyle.italic; - } - if (attributes?['underline'] == true) { - decoration = TextDecoration.underline; - } - if (attributes?['strikethrough'] == true) { - decoration = TextDecoration.lineThrough; - } - if (attributes?['highlight'] is String) { - highLightColor = Color(int.parse(attributes!['highlight'])); - } - if (attributes?['href'] is String) { - color = const Color.fromARGB(255, 55, 120, 245); - decoration = TextDecoration.underline; - gestureRecognizer = TapGestureRecognizer() - ..onTap = () { - launchUrlString(attributes?['href']); - }; - } - final heading = attributes?['heading'] as String?; - if (heading != null) { - // TODO: make it better - if (heading == 'h1') { - fontSize = 30.0; - } else if (heading == 'h2') { - fontSize = 20.0; - } - fontWeight = FontWeight.bold; - } - return TextSpan( - text: content, - style: TextStyle( - fontWeight: fontWeight, - fontStyle: fontStyle, - decoration: decoration, - color: color, - fontSize: fontSize, - backgroundColor: highLightColor, - ), - recognizer: gestureRecognizer, - ); - } -} - -TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) { - if (globalSel == null) { - return null; - } - final nodePath = node.path; - - if (!pathEquals(nodePath, globalSel.start.path)) { - return null; - } - if (globalSel.isCollapsed) { - return TextSelection( - baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset); - } else { - if (pathEquals(globalSel.start.path, globalSel.end.path)) { - return TextSelection( - baseOffset: globalSel.start.offset, - extentOffset: globalSel.end.offset); - } - } - return null; -} - -Selection? _localSelectionToGlobal(Node node, TextSelection? sel) { - if (sel == null) { - return null; - } - final nodePath = node.path; - - return Selection( - start: Position(path: nodePath, offset: sel.baseOffset), - end: Position(path: nodePath, offset: sel.extentOffset), - ); -} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart index ff6c6e9932..f7985ed564 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart @@ -12,7 +12,7 @@ class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder { bool get isCompleted => node.attributes['checkbox'] as bool; @override - Widget build(BuildContext buildContext) { + Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -20,7 +20,7 @@ class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder { Expanded( child: renderPlugins.buildWidget( context: NodeWidgetContext( - buildContext: buildContext, + buildContext: context, node: node, editorState: editorState, ), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart index 22022a65ec..c4bd027888 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart @@ -27,13 +27,13 @@ class TextWithHeadingNodeBuilder extends NodeWidgetBuilder { } @override - Widget build(BuildContext buildContext) { + Widget build(BuildContext context) { return Column( children: [ buildPadding(), renderPlugins.buildWidget( context: NodeWidgetContext( - buildContext: buildContext, + buildContext: context, node: node, editorState: editorState, ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index cd503843c2..e388ea3661 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/service/service.dart'; import 'package:flutter/material.dart'; @@ -25,6 +26,7 @@ class ApplyOptions { class EditorState { final StateTree document; final RenderPlugins renderPlugins; + List selectedNodes = []; // Service reference. @@ -39,6 +41,8 @@ class EditorState { required this.document, required this.renderPlugins, }) { + // FIXME: abstract render plugins as a service. + renderPlugins.register('text', RichTextNodeWidgetBuilder.create); undoManager.state = this; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 3f8510d8b3..91c6b1c4b0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -12,3 +12,5 @@ export 'package:flowy_editor/operation/transaction_builder.dart'; export 'package:flowy_editor/operation/operation.dart'; export 'package:flowy_editor/editor_state.dart'; export 'package:flowy_editor/service/editor_service.dart'; +export 'package:flowy_editor/document/selection.dart'; +export 'package:flowy_editor/document/position.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index 659c380720..214818f60a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -26,14 +26,14 @@ class NodeWidgetBuilder { /// Render the current [Node] /// and the layout style of [Node.Children]. Widget build( - BuildContext buildContext, + BuildContext context, ) => throw UnimplementedError(); /// TODO: refactore this part. /// return widget embedded with ChangeNotifier and widget itself. Widget call( - BuildContext buildContext, + BuildContext context, ) { /// TODO: Validate the node /// if failed, stop call build function, @@ -43,10 +43,10 @@ class NodeWidgetBuilder { 'Node validate failure, node = { type: ${node.type}, attributes: ${node.attributes} }'); } - return _build(buildContext); + return _build(context); } - Widget _build(BuildContext buildContext) { + Widget _build(BuildContext context) { return CompositedTransformTarget( link: node.layerLink, child: ChangeNotifierProvider.value( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart new file mode 100644 index 0000000000..5f2ca0689e --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -0,0 +1,215 @@ +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { + RichTextNodeWidgetBuilder.create({ + required super.editorState, + required super.node, + required super.key, + }) : super.create(); + + @override + Widget build(BuildContext context) { + return FlowyRichText( + key: key, + textNode: node as TextNode, + editorState: editorState, + ); + } +} + +class FlowyRichText extends StatefulWidget { + const FlowyRichText({ + Key? key, + this.cursorHeight, + this.cursorWidth = 2.0, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final double? cursorHeight; + final double cursorWidth; + final TextNode textNode; + final EditorState editorState; + + @override + State createState() => _FlowyRichTextState(); +} + +class _FlowyRichTextState extends State with Selectable { + final _textKey = GlobalKey(); + final _decorationKey = GlobalKey(); + + EditorState get _editorState => widget.editorState; + TextNode get _textNode => widget.textNode; + RenderParagraph get _renderParagraph => + _textKey.currentContext?.findRenderObject() as RenderParagraph; + + @override + Widget build(BuildContext context) { + final attributes = _textNode.attributes; + // TODO: use factory method ?? + if (attributes.list == 'todo') { + return _buildTodoListRichText(context); + } else if (attributes.list == 'bullet') { + return _buildBulletedListRichText(context); + } else if (attributes.quotes == true) { + return _buildQuotedRichText(context); + } + return _buildRichText(context); + } + + @override + Position start() => Position(path: _textNode.path, offset: 0); + + @override + Position end() => + Position(path: _textNode.path, offset: _textNode.toRawString().length); + + @override + Rect getCursorRectInPosition(Position position) { + final textPosition = TextPosition(offset: position.offset); + final baseRect = frontWidgetRect(); + final cursorOffset = + _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); + final cursorHeight = widget.cursorHeight ?? + _renderParagraph.getFullHeightForCaret(textPosition) ?? + 5.0; // default height + return Rect.fromLTWH( + baseRect.centerRight.dx + cursorOffset.dx - (widget.cursorWidth / 2), + cursorOffset.dy, + widget.cursorWidth, + cursorHeight, + ); + } + + @override + Position getPositionInOffset(Offset start) { + final offset = _renderParagraph.globalToLocal(start); + final baseOffset = _renderParagraph.getPositionForOffset(offset).offset; + return Position(path: _textNode.path, offset: baseOffset); + } + + @override + List getRectsInSelection(Selection selection) { + assert(pathEquals(selection.start.path, selection.end.path) && + pathEquals(selection.start.path, _textNode.path)); + + final textSelection = TextSelection( + baseOffset: selection.start.offset, + extentOffset: selection.end.offset, + ); + final baseRect = frontWidgetRect(); + return _renderParagraph.getBoxesForSelection(textSelection).map((box) { + final rect = box.toRect(); + return rect.translate(baseRect.centerRight.dx, 0); + }).toList(); + } + + @override + Selection getSelectionInRange(Offset start, Offset end) { + final localStart = _renderParagraph.globalToLocal(start); + final localEnd = _renderParagraph.globalToLocal(end); + final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset; + final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset; + return Selection.single( + path: _textNode.path, + startOffset: baseOffset, + endOffset: extentOffset, + ); + } + + Widget _buildRichText(BuildContext context) { + if (_textNode.children.isEmpty) { + return _buildSingleRichText(context); + } else { + return _buildRichTextWithChildren(context); + } + } + + Widget _buildRichTextWithChildren(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSingleRichText(context), + ..._textNode.children + .map( + (child) => _editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: child, + editorState: _editorState, + ), + ), + ) + .toList() + ], + ); + } + + Widget _buildSingleRichText(BuildContext context) { + return Expanded(child: RichText(key: _textKey, text: _textSpan)); + } + + Widget _buildTodoListRichText(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + child: Icon( + key: _decorationKey, + _textNode.attributes.todo + ? Icons.square_rounded + : Icons.square_outlined), + onTap: () => TransactionBuilder(_editorState) + ..updateNode(_textNode, { + 'todo': !_textNode.attributes.todo, + }) + ..commit(), + ), + _buildRichText(context), + ], + ); + } + + Widget _buildBulletedListRichText(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(key: _decorationKey, Icons.circle), + _buildRichText(context), + ], + ); + } + + Widget _buildQuotedRichText(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(key: _decorationKey, Icons.format_quote), + _buildRichText(context), + ], + ); + } + + Rect frontWidgetRect() { + // FIXME: find a more elegant way to solve this situation. + if (_textNode.attributes.list != null) { + final renderBox = + _decorationKey.currentContext?.findRenderObject() as RenderBox; + return renderBox.localToGlobal(Offset.zero) & renderBox.size; + } + return Rect.zero; + } + + TextSpan get _textSpan => TextSpan( + children: _textNode.delta.operations + .whereType() + .map((insert) => RichTextStyle( + attributes: insert.attributes ?? {}, + text: insert.content, + ).toTextSpan()) + .toList(growable: false)); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart new file mode 100644 index 0000000000..a1fd8b57a1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -0,0 +1,182 @@ +import 'package:flowy_editor/document/attributes.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class StyleKey { + static String bold = 'bold'; + static String italic = 'italic'; + static String underline = 'underline'; + static String strikethrough = 'strikethrough'; + static String color = 'color'; + static String font = 'font'; + static String href = 'href'; + static String heading = 'heading'; + static String quotes = 'quotes'; + static String list = 'list'; + static String todo = 'todo'; + static String code = 'code'; +} + +extension AttributesExtensions on Attributes { + bool get bold { + return (containsKey(StyleKey.bold) && this[StyleKey.bold] == true); + } + + bool get italic { + return (containsKey(StyleKey.italic) && this[StyleKey.italic] == true); + } + + bool get underline { + return (containsKey(StyleKey.underline) && + this[StyleKey.underline] == true); + } + + bool get strikethrough { + return (containsKey(StyleKey.strikethrough) && + this[StyleKey.strikethrough] == true); + } + + Color? get color { + if (containsKey(StyleKey.color) && this[StyleKey.color] is String) { + return Color( + int.parse(this[StyleKey.color]), + ); + } + return null; + } + + String? get font { + // TODO: unspport now. + return null; + } + + String? get href { + if (containsKey(StyleKey.href) && this[StyleKey.href] is String) { + return this[StyleKey.href]; + } + return null; + } + + String? get heading { + if (containsKey(StyleKey.heading) && this[StyleKey.heading] is String) { + return this[StyleKey.heading]; + } + return null; + } + + bool get quotes { + if (containsKey(StyleKey.quotes) && this[StyleKey.quotes] == true) { + return this[StyleKey.quotes]; + } + return false; + } + + String? get list { + if (containsKey(StyleKey.list) && this[StyleKey.list] is String) { + return this[StyleKey.list]; + } + return null; + } + + bool get todo { + if (containsKey(StyleKey.todo) && this[StyleKey.todo] is bool) { + return this[StyleKey.todo]; + } + return false; + } + + bool get code { + if (containsKey(StyleKey.code) && this[StyleKey.code] == true) { + return this[StyleKey.code]; + } + return false; + } +} + +/// +/// Supported partial rendering types: +/// bold, italic, +/// underline, strikethrough, +/// color, font, +/// href +/// +/// Supported global rendering types: +/// heading: h1, h2, h3, h4, h5, h6, +/// block quotes, +/// list: ordered list, bulleted list, +/// code block +/// +class RichTextStyle { + // TODO: customize + RichTextStyle({ + required this.attributes, + required this.text, + }); + + final Attributes attributes; + final String text; + + TextSpan toTextSpan() { + return TextSpan( + text: text, + style: TextStyle( + fontWeight: fontWeight, + fontStyle: fontStyle, + fontSize: fontSize, + color: textColor, + decoration: textDecoration, + ), + recognizer: recognizer, + ); + } + + // bold + FontWeight get fontWeight => + attributes.bold ? FontWeight.bold : FontWeight.normal; + + // underline or strikethrough + TextDecoration get textDecoration { + if (attributes.underline || attributes.href != null) { + return TextDecoration.underline; + } else if (attributes.strikethrough) { + return TextDecoration.lineThrough; + } + return TextDecoration.none; + } + + // font + FontStyle get fontStyle => + attributes.italic ? FontStyle.italic : FontStyle.normal; + + // text color + Color get textColor { + if (attributes.href != null) { + return Colors.lightBlue; + } + return attributes.color ?? Colors.black; + } + + // font size + double get fontSize { + final heading = attributes.heading; + if (heading != null) { + final headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + final fontSizes = [30.0, 28.0, 26.0, 24.0, 22.0, 20.0]; + return fontSizes[headings.indexOf(heading)]; + } else { + return 18.0; + } + } + + // recognizer + GestureRecognizer? get recognizer { + final href = attributes.href; + if (href != null) { + return TapGestureRecognizer() + ..onTap = () async { + // FIXME: launch the url + }; + } + return null; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 4d155972df..8dfea75135 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; /// mixin Selectable on State { - /// Returns a [List] of the [Rect] selection sorrounded by start and end + /// Returns a [List] of the [Rect] selection surrounded by start and end /// in current widget. /// /// [start] and [end] are the offsets under the global coordinate system. @@ -32,12 +32,5 @@ mixin Selectable on State { /// /// Only the widget rendered by [TextNode] need to implement the detail, /// and the rest can return null. - TextSelection? getCurrentTextSelection() => null; - - /// For [TextNode] only. - /// - /// Retruns a [Offset]. - /// Only the widget rendered by [TextNode] need to implement the detail, - /// and the rest can return [Offset.zero]. - Offset getOffsetByTextSelection(TextSelection textSelection) => Offset.zero; + TextSelection? getTextSelectionInSelection(Selection selection) => null; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart index 47a83f314a..f5da6423ae 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart @@ -12,58 +12,58 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { return KeyEventResult.ignored; } - final selectionNodes = editorState.selectedNodes; - if (selectionNodes.length == 1 && selectionNodes.first is TextNode) { - final node = selectionNodes.first.unwrapOrNull(); - final selectable = node?.key?.currentState?.unwrapOrNull(); - if (selectable != null) { - final textSelection = selectable.getCurrentTextSelection(); - if (textSelection != null) { - if (textSelection.isCollapsed) { - /// Three cases: - /// Delete the zero character, - /// 1. if there is still text node in front of it, then merge them. - /// 2. if not, just ignore - /// Delete the non-zero character, - /// 3. delete the single character. - if (textSelection.baseOffset == 0) { - if (node?.previous != null && node?.previous is TextNode) { - final previous = node!.previous! as TextNode; - final newTextSelection = TextSelection.collapsed( - offset: previous.toRawString().length); - final selectionService = editorState.service.selectionService; - final previousSelectable = - previous.key?.currentState?.unwrapOrNull(); - final newOfset = previousSelectable - ?.getOffsetByTextSelection(newTextSelection); - if (newOfset != null) { - // selectionService.updateCursor(newOfset); - } - // merge - TransactionBuilder(editorState) - ..deleteNode(node) - ..insertText( - previous, previous.toRawString().length, node.toRawString()) - ..commit(); - return KeyEventResult.handled; - } else { - return KeyEventResult.ignored; - } - } else { - TransactionBuilder(editorState) - ..deleteText(node!, textSelection.baseOffset - 1, 1) - ..commit(); - final newTextSelection = - TextSelection.collapsed(offset: textSelection.baseOffset - 1); - final selectionService = editorState.service.selectionService; - final newOfset = - selectable.getOffsetByTextSelection(newTextSelection); - // selectionService.updateCursor(newOfset); - return KeyEventResult.handled; - } - } - } - } - } + // final selectionNodes = editorState.selectedNodes; + // if (selectionNodes.length == 1 && selectionNodes.first is TextNode) { + // final node = selectionNodes.first.unwrapOrNull(); + // final selectable = node?.key?.currentState?.unwrapOrNull(); + // if (selectable != null) { + // final textSelection = selectable.getCurrentTextSelection(); + // if (textSelection != null) { + // if (textSelection.isCollapsed) { + // /// Three cases: + // /// Delete the zero character, + // /// 1. if there is still text node in front of it, then merge them. + // /// 2. if not, just ignore + // /// Delete the non-zero character, + // /// 3. delete the single character. + // if (textSelection.baseOffset == 0) { + // if (node?.previous != null && node?.previous is TextNode) { + // final previous = node!.previous! as TextNode; + // final newTextSelection = TextSelection.collapsed( + // offset: previous.toRawString().length); + // final selectionService = editorState.service.selectionService; + // final previousSelectable = + // previous.key?.currentState?.unwrapOrNull(); + // final newOfset = previousSelectable + // ?.getOffsetByTextSelection(newTextSelection); + // if (newOfset != null) { + // // selectionService.updateCursor(newOfset); + // } + // // merge + // TransactionBuilder(editorState) + // ..deleteNode(node) + // ..insertText( + // previous, previous.toRawString().length, node.toRawString()) + // ..commit(); + // return KeyEventResult.handled; + // } else { + // return KeyEventResult.ignored; + // } + // } else { + // TransactionBuilder(editorState) + // ..deleteText(node!, textSelection.baseOffset - 1, 1) + // ..commit(); + // final newTextSelection = + // TextSelection.collapsed(offset: textSelection.baseOffset - 1); + // final selectionService = editorState.service.selectionService; + // final newOfset = + // selectable.getOffsetByTextSelection(newTextSelection); + // // selectionService.updateCursor(newOfset); + // return KeyEventResult.handled; + // } + // } + // } + // } + // } return KeyEventResult.ignored; }; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart index 3eef8c1d1b..f424bcf314 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart @@ -1,6 +1,4 @@ -import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10,21 +8,5 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { return KeyEventResult.ignored; } - final selectedNodes = editorState.selectedNodes; - if (selectedNodes.length != 1) { - return KeyEventResult.ignored; - } - - final textNode = selectedNodes.first.unwrapOrNull(); - final selectable = textNode?.key?.currentState?.unwrapOrNull(); - final textSelection = selectable?.getCurrentTextSelection(); - // if (textNode != null && selectable != null && textSelection != null) { - // final offset = selectable.getOffsetByTextSelection(textSelection); - // final rect = selectable.getCursorRect(offset); - // editorState.service.floatingToolbarService - // .showInOffset(rect.topLeft, textNode.layerLink); - // return KeyEventResult.handled; - // } - return KeyEventResult.ignored; }; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 52c0b84f2c..07cf2ad902 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -299,6 +299,9 @@ class _FlowySelectionState extends State panEndOffset = details.globalPosition; final nodes = getNodesInRange(panStartOffset!, panEndOffset!); + if (nodes.isEmpty) { + return; + } final first = nodes.first.selectable; final last = nodes.last.selectable; From c5560caf3c2a692920dcba883b682507db72a1de Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 27 Jul 2022 22:06:32 +0800 Subject: [PATCH 076/121] feat: import SvgPicture package to display SVG --- .../flowy_editor/assets/images/check.svg | 4 ++ .../flowy_editor/assets/images/uncheck.svg | 3 ++ .../flowy_editor/example/pubspec.lock | 37 ++++++++++++++++++- .../flowy_editor/lib/infra/flowy_svg.dart | 27 ++++++++++++++ .../lib/render/rich_text/flowy_rich_text.dart | 12 +++--- .../packages/flowy_editor/pubspec.yaml | 4 +- 6 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/check.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/uncheck.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/check.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/check.svg new file mode 100644 index 0000000000..8446cced9f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/uncheck.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/uncheck.svg new file mode 100644 index 0000000000..6c487795c6 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/uncheck.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock index 6dd22ff45f..cfadcb8242 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock @@ -76,6 +76,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1+1" flutter_test: dependency: "direct dev" description: flutter @@ -135,6 +142,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" plugin_platform_interface: dependency: transitive description: @@ -259,6 +287,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" sdks: dart: ">=2.17.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=2.11.0-0.1.pre" diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart new file mode 100644 index 0000000000..5e3861e6f4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class FlowySvg extends StatelessWidget { + const FlowySvg({ + Key? key, + required this.name, + required this.size, + this.color, + }) : super(key: key); + + final String name; + final Size size; + final Color? color; + + @override + Widget build(BuildContext context) { + return SizedBox.fromSize( + size: size, + child: SvgPicture.asset( + 'assets/images/$name.svg', + color: color, + package: 'flowy_editor', + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 5f2ca0689e..c0266a15bc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -2,6 +2,7 @@ import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { RichTextNodeWidgetBuilder.create({ @@ -154,15 +155,16 @@ class _FlowyRichTextState extends State with Selectable { } Widget _buildTodoListRichText(BuildContext context) { + final name = _textNode.attributes.todo ? 'check' : 'uncheck'; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( - child: Icon( - key: _decorationKey, - _textNode.attributes.todo - ? Icons.square_rounded - : Icons.square_outlined), + child: FlowySvg( + name: name, + key: _decorationKey, + size: const Size.square(20), + ), onTap: () => TransactionBuilder(_editorState) ..updateNode(_textNode, { 'todo': !_textNode.attributes.todo, diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 74ca437e27..08e51118d1 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: flutter: sdk: flutter + flutter_svg: ^1.1.1+1 provider: ^6.0.3 dev_dependencies: @@ -26,7 +27,8 @@ flutter: # To add assets to your package, add an assets section, like this: assets: - - document.json + - assets/images/uncheck.svg + - assets/images/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # From 985fe14a8ba4fb9aff9e8319867c3b0560f918de Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 28 Jul 2022 11:41:39 +0800 Subject: [PATCH 077/121] feat: update RichText render style --- .../flowy_editor/assets/images/point.svg | 3 + .../flowy_editor/assets/images/quote.svg | 3 + .../flowy_editor/example/assets/example.json | 207 ++++++++++++++++++ .../flowy_editor/example/lib/main.dart | 6 +- .../example/lib/plugin/image_node_widget.dart | 5 +- .../flowy_editor/example/pubspec.yaml | 1 + .../flowy_editor/lib/infra/flowy_svg.dart | 34 ++- .../lib/render/rich_text/flowy_rich_text.dart | 73 +++++- .../lib/render/rich_text/rich_text_style.dart | 50 ++++- 9 files changed, 346 insertions(+), 36 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/point.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/example/assets/example.json diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/point.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/point.svg new file mode 100644 index 0000000000..be88518d0d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/point.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg new file mode 100644 index 0000000000..0f3d33f6d3 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json new file mode 100644 index 0000000000..a552175146 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -0,0 +1,207 @@ +{ + "document": { + "type": "editor", + "attributes": {}, + "children": [ + { + "type": "image", + "attributes": { + "image_src": "https://images.pexels.com/photos/2253275/pexels-photo-2253275.jpeg?cs=srgb&dl=pexels-helena-lopes-2253275.jpg&fm=jpg" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "🌶 Read Me", + "attributes": { + "heading": "h1" + } + } + ], + "attributes": { + "heading": "h1" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "👋 Welcome to Appflowy", + "attributes": { + "heading": "h2" + } + } + ], + "attributes": { + "heading": "h2" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Here are the basics:", + "attributes": { + "heading": "h3" + } + } + ], + "attributes": { + "heading": "h3" + } + }, + { + "type": "text", + "delta": [ + { "insert": "Click " }, + { "insert": "anywhere", "attributes": { "underline": true } }, + { "insert": " and just typing." } + ], + "attributes": { + "list": "todo", + "todo": true + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hit" + }, + { + "insert": " / ", + "attributes": { "highlightColor": "0xFFFFFF00" } + }, + { + "insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc." + } + ], + "attributes": { + "list": "todo", + "todo": true + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Highlight any text, and use the menu that pops up to " + }, + { "insert": "style", "attributes": { "bold": true } }, + { "insert": " your ", "attributes": { "italic": true } }, + { "insert": "writing", "attributes": { "strikethrough": true } }, + { "insert": "." } + ], + "attributes": { + "list": "todo", + "todo": true + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Here are the examples:", + "attributes": { + "heading": "h3" + } + } + ], + "attributes": { + "heading": "h3" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "list": "bullet" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "list": "bullet" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "list": "bullet" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world", + "attributes": { "quote": true } + } + ], + "attributes": { + "quote": true + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world", + "attributes": { "quote": true } + } + ], + "attributes": { + "quote": true + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "number": 1 + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "number": 2 + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "number": 3 + } + } + ] + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index b8b836cc4d..6105703fa0 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -112,14 +112,14 @@ class _MyHomePageState extends State { if (page == 0) { return _buildFlowyEditor(); } else if (page == 1) { - return _buildTextfield(); + return _buildTextField(); } return Container(); } Widget _buildFlowyEditor() { return FutureBuilder( - future: rootBundle.loadString('assets/document.json'), + future: rootBundle.loadString('assets/example.json'), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center( @@ -167,7 +167,7 @@ class _MyHomePageState extends State { ); } - Widget _buildTextfield() { + Widget _buildTextField() { return const Center( child: TextField(), ); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index aaca3148c2..e33ff83e2f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -83,7 +83,10 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { Widget _build(BuildContext context) { return Column( children: [ - Image.network(src), + Image.network( + src, + height: 150.0, + ), if (node.children.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml index 11df9b36ee..9a80a73a0a 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml @@ -64,6 +64,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - document.json + - example.json # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart index 5e3861e6f4..136b5db4bc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart @@ -4,24 +4,36 @@ import 'package:flutter_svg/svg.dart'; class FlowySvg extends StatelessWidget { const FlowySvg({ Key? key, - required this.name, - required this.size, + this.name, + this.size = const Size(20, 20), this.color, + this.number, }) : super(key: key); - final String name; + final String? name; final Size size; final Color? color; + final int? number; @override Widget build(BuildContext context) { - return SizedBox.fromSize( - size: size, - child: SvgPicture.asset( - 'assets/images/$name.svg', - color: color, - package: 'flowy_editor', - ), - ); + if (name != null) { + return SizedBox.fromSize( + size: size, + child: SvgPicture.asset( + 'assets/images/$name.svg', + color: color, + package: 'flowy_editor', + ), + ); + } else if (number != null) { + final numberText = + '$number.'; + return SizedBox.fromSize( + size: size, + child: SvgPicture.string(numberText), + ); + } + return Container(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index c0266a15bc..d90e739d97 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -1,8 +1,19 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/node_widget_builder.dart'; +import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { RichTextNodeWidgetBuilder.create({ @@ -56,8 +67,12 @@ class _FlowyRichTextState extends State with Selectable { return _buildTodoListRichText(context); } else if (attributes.list == 'bullet') { return _buildBulletedListRichText(context); - } else if (attributes.quotes == true) { + } else if (attributes.quote == true) { return _buildQuotedRichText(context); + } else if (attributes.heading != null) { + return _buildHeadingRichText(context); + } else if (attributes.number != null) { + return _buildNumberListRichText(context); } return _buildRichText(context); } @@ -151,7 +166,11 @@ class _FlowyRichTextState extends State with Selectable { } Widget _buildSingleRichText(BuildContext context) { - return Expanded(child: RichText(key: _textKey, text: _textSpan)); + return SizedBox( + width: + MediaQuery.of(context).size.width - 20, // FIXME: use the const value + child: RichText(key: _textKey, text: _textSpan), + ); } Widget _buildTodoListRichText(BuildContext context) { @@ -161,9 +180,8 @@ class _FlowyRichTextState extends State with Selectable { children: [ GestureDetector( child: FlowySvg( - name: name, key: _decorationKey, - size: const Size.square(20), + name: name, ), onTap: () => TransactionBuilder(_editorState) ..updateNode(_textNode, { @@ -178,9 +196,25 @@ class _FlowyRichTextState extends State with Selectable { Widget _buildBulletedListRichText(BuildContext context) { return Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(key: _decorationKey, Icons.circle), + FlowySvg( + key: _decorationKey, + name: 'point', + ), + _buildRichText(context), + ], + ); + } + + Widget _buildNumberListRichText(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FlowySvg( + key: _decorationKey, + number: _textNode.attributes.number, + ), _buildRichText(context), ], ); @@ -190,17 +224,32 @@ class _FlowyRichTextState extends State with Selectable { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(key: _decorationKey, Icons.format_quote), + FlowySvg( + key: _decorationKey, + name: 'quote', + ), _buildRichText(context), ], ); } + Widget _buildHeadingRichText(BuildContext context) { + // TODO: customize + return Column( + children: [ + const Padding(padding: EdgeInsets.only(top: 5)), + _buildRichText(context), + const Padding(padding: EdgeInsets.only(top: 5)), + ], + ); + } + Rect frontWidgetRect() { // FIXME: find a more elegant way to solve this situation. - if (_textNode.attributes.list != null) { - final renderBox = - _decorationKey.currentContext?.findRenderObject() as RenderBox; + final renderBox = _decorationKey.currentContext + ?.findRenderObject() + ?.unwrapOrNull(); + if (renderBox != null) { return renderBox.localToGlobal(Offset.zero) & renderBox.size; } return Rect.zero; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index a1fd8b57a1..07d4bf4429 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -8,11 +8,13 @@ class StyleKey { static String underline = 'underline'; static String strikethrough = 'strikethrough'; static String color = 'color'; + static String highlightColor = 'highlightColor'; static String font = 'font'; static String href = 'href'; static String heading = 'heading'; - static String quotes = 'quotes'; + static String quote = 'quote'; static String list = 'list'; + static String number = 'number'; static String todo = 'todo'; static String code = 'code'; } @@ -45,6 +47,16 @@ extension AttributesExtensions on Attributes { return null; } + Color? get hightlightColor { + if (containsKey(StyleKey.highlightColor) && + this[StyleKey.highlightColor] is String) { + return Color( + int.parse(this[StyleKey.highlightColor]), + ); + } + return null; + } + String? get font { // TODO: unspport now. return null; @@ -64,9 +76,9 @@ extension AttributesExtensions on Attributes { return null; } - bool get quotes { - if (containsKey(StyleKey.quotes) && this[StyleKey.quotes] == true) { - return this[StyleKey.quotes]; + bool get quote { + if (containsKey(StyleKey.quote) && this[StyleKey.quote] == true) { + return this[StyleKey.quote]; } return false; } @@ -78,6 +90,13 @@ extension AttributesExtensions on Attributes { return null; } + int? get number { + if (containsKey(StyleKey.number) && this[StyleKey.number] is int) { + return this[StyleKey.number]; + } + return null; + } + bool get todo { if (containsKey(StyleKey.todo) && this[StyleKey.todo] is bool) { return this[StyleKey.todo]; @@ -102,7 +121,7 @@ extension AttributesExtensions on Attributes { /// /// Supported global rendering types: /// heading: h1, h2, h3, h4, h5, h6, -/// block quotes, +/// block quote, /// list: ordered list, bulleted list, /// code block /// @@ -124,6 +143,7 @@ class RichTextStyle { fontStyle: fontStyle, fontSize: fontSize, color: textColor, + backgroundColor: backgroundColor, decoration: textDecoration, ), recognizer: recognizer, @@ -131,8 +151,14 @@ class RichTextStyle { } // bold - FontWeight get fontWeight => - attributes.bold ? FontWeight.bold : FontWeight.normal; + FontWeight get fontWeight { + if (attributes.bold) { + return FontWeight.bold; + } else if (attributes.heading != null) { + return FontWeight.bold; + } + return FontWeight.normal; + } // underline or strikethrough TextDecoration get textDecoration { @@ -152,19 +178,25 @@ class RichTextStyle { Color get textColor { if (attributes.href != null) { return Colors.lightBlue; + } else if (attributes.quote) { + return Colors.grey; } return attributes.color ?? Colors.black; } + Color get backgroundColor { + return attributes.hightlightColor ?? Colors.transparent; + } + // font size double get fontSize { final heading = attributes.heading; if (heading != null) { final headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - final fontSizes = [30.0, 28.0, 26.0, 24.0, 22.0, 20.0]; + final fontSizes = [30.0, 25.0, 20.0, 20.0, 20.0, 20.0]; return fontSizes[headings.indexOf(heading)]; } else { - return 18.0; + return 16.0; } } From 612e3dd50fc19c6188d992b7a2a2198ac1f7fb40 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 28 Jul 2022 12:10:19 +0800 Subject: [PATCH 078/121] feat: decorate TextSpan with global style --- .../flowy_editor/example/assets/example.json | 26 +-- .../lib/render/rich_text/flowy_rich_text.dart | 18 ++- .../lib/render/rich_text/rich_text_style.dart | 153 ++++++++++-------- 3 files changed, 108 insertions(+), 89 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index a552175146..0dad009cd2 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -13,10 +13,7 @@ "type": "text", "delta": [ { - "insert": "🌶 Read Me", - "attributes": { - "heading": "h1" - } + "insert": "🌶 Read Me" } ], "attributes": { @@ -27,10 +24,7 @@ "type": "text", "delta": [ { - "insert": "👋 Welcome to Appflowy", - "attributes": { - "heading": "h2" - } + "insert": "👋 Welcome to Appflowy" } ], "attributes": { @@ -41,10 +35,7 @@ "type": "text", "delta": [ { - "insert": "Here are the basics:", - "attributes": { - "heading": "h3" - } + "insert": "Here are the basics:" } ], "attributes": { @@ -102,10 +93,7 @@ "type": "text", "delta": [ { - "insert": "Here are the examples:", - "attributes": { - "heading": "h3" - } + "insert": "Here are the examples:" } ], "attributes": { @@ -149,8 +137,7 @@ "type": "text", "delta": [ { - "insert": "Hello world", - "attributes": { "quote": true } + "insert": "Hello world" } ], "attributes": { @@ -161,8 +148,7 @@ "type": "text", "delta": [ { - "insert": "Hello world", - "attributes": { "quote": true } + "insert": "Hello world" } ], "attributes": { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index d90e739d97..66c87a2dd4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -169,7 +169,7 @@ class _FlowyRichTextState extends State with Selectable { return SizedBox( width: MediaQuery.of(context).size.width - 20, // FIXME: use the const value - child: RichText(key: _textKey, text: _textSpan), + child: RichText(key: _textKey, text: _decorateTextSpanWithGlobalStyle), ); } @@ -255,6 +255,22 @@ class _FlowyRichTextState extends State with Selectable { return Rect.zero; } + TextSpan get _decorateTextSpanWithGlobalStyle => TextSpan( + children: _textSpan.children + ?.whereType() + .map( + (span) => TextSpan( + text: span.text, + style: span.style?.copyWith( + fontSize: _textNode.attributes.fontSize, + color: _textNode.attributes.quoteColor, + ), + recognizer: span.recognizer, + ), + ) + .toList(), + ); + TextSpan get _textSpan => TextSpan( children: _textNode.delta.operations .whereType() diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index 07d4bf4429..b4100f9b87 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -2,6 +2,19 @@ import 'package:flowy_editor/document/attributes.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +/// +/// Supported partial rendering types: +/// bold, italic, +/// underline, strikethrough, +/// color, font, +/// href +/// +/// Supported global rendering types: +/// heading: h1, h2, h3, h4, h5, h6, ... +/// block quote, +/// list: ordered list, bulleted list, +/// code block +/// class StyleKey { static String bold = 'bold'; static String italic = 'italic'; @@ -11,6 +24,7 @@ class StyleKey { static String highlightColor = 'highlightColor'; static String font = 'font'; static String href = 'href'; + static String heading = 'heading'; static String quote = 'quote'; static String list = 'list'; @@ -19,7 +33,76 @@ class StyleKey { static String code = 'code'; } -extension AttributesExtensions on Attributes { +double baseFontSize = 16.0; +// TODO: customize. +Map headingToFontSize = { + 'h1': baseFontSize + 15, + 'h2': baseFontSize + 12, + 'h3': baseFontSize + 9, + 'h4': baseFontSize + 6, + 'h5': baseFontSize + 3, + 'h6': baseFontSize, +}; + +extension NodeAttributesExtensions on Attributes { + String? get heading { + if (containsKey(StyleKey.heading) && this[StyleKey.heading] is String) { + return this[StyleKey.heading]; + } + return null; + } + + double get fontSize { + if (heading != null) { + return headingToFontSize[heading]!; + } + return baseFontSize; + } + + bool get quote { + if (containsKey(StyleKey.quote) && this[StyleKey.quote] == true) { + return this[StyleKey.quote]; + } + return false; + } + + Color? get quoteColor { + if (quote) { + return Colors.grey; + } + return null; + } + + String? get list { + if (containsKey(StyleKey.list) && this[StyleKey.list] is String) { + return this[StyleKey.list]; + } + return null; + } + + int? get number { + if (containsKey(StyleKey.number) && this[StyleKey.number] is int) { + return this[StyleKey.number]; + } + return null; + } + + bool get todo { + if (containsKey(StyleKey.todo) && this[StyleKey.todo] is bool) { + return this[StyleKey.todo]; + } + return false; + } + + bool get code { + if (containsKey(StyleKey.code) && this[StyleKey.code] == true) { + return this[StyleKey.code]; + } + return false; + } +} + +extension DeltaAttributesExtensions on Attributes { bool get bold { return (containsKey(StyleKey.bold) && this[StyleKey.bold] == true); } @@ -68,63 +151,8 @@ extension AttributesExtensions on Attributes { } return null; } - - String? get heading { - if (containsKey(StyleKey.heading) && this[StyleKey.heading] is String) { - return this[StyleKey.heading]; - } - return null; - } - - bool get quote { - if (containsKey(StyleKey.quote) && this[StyleKey.quote] == true) { - return this[StyleKey.quote]; - } - return false; - } - - String? get list { - if (containsKey(StyleKey.list) && this[StyleKey.list] is String) { - return this[StyleKey.list]; - } - return null; - } - - int? get number { - if (containsKey(StyleKey.number) && this[StyleKey.number] is int) { - return this[StyleKey.number]; - } - return null; - } - - bool get todo { - if (containsKey(StyleKey.todo) && this[StyleKey.todo] is bool) { - return this[StyleKey.todo]; - } - return false; - } - - bool get code { - if (containsKey(StyleKey.code) && this[StyleKey.code] == true) { - return this[StyleKey.code]; - } - return false; - } } -/// -/// Supported partial rendering types: -/// bold, italic, -/// underline, strikethrough, -/// color, font, -/// href -/// -/// Supported global rendering types: -/// heading: h1, h2, h3, h4, h5, h6, -/// block quote, -/// list: ordered list, bulleted list, -/// code block -/// class RichTextStyle { // TODO: customize RichTextStyle({ @@ -154,8 +182,6 @@ class RichTextStyle { FontWeight get fontWeight { if (attributes.bold) { return FontWeight.bold; - } else if (attributes.heading != null) { - return FontWeight.bold; } return FontWeight.normal; } @@ -178,8 +204,6 @@ class RichTextStyle { Color get textColor { if (attributes.href != null) { return Colors.lightBlue; - } else if (attributes.quote) { - return Colors.grey; } return attributes.color ?? Colors.black; } @@ -190,14 +214,7 @@ class RichTextStyle { // font size double get fontSize { - final heading = attributes.heading; - if (heading != null) { - final headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - final fontSizes = [30.0, 25.0, 20.0, 20.0, 20.0, 20.0]; - return fontSizes[headings.indexOf(heading)]; - } else { - return 16.0; - } + return baseFontSize; } // recognizer From 0ba7c53dad5dd1eafd7258b840a2dc33ad2fab3d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 14:41:29 +0800 Subject: [PATCH 079/121] feat: remove unused imports --- .../flowy_editor/lib/render/rich_text/flowy_rich_text.dart | 4 ++++ .../internal_key_event_handlers/arrow_keys_handler.dart | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 66c87a2dd4..4731542ae2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -255,6 +255,10 @@ class _FlowyRichTextState extends State with Selectable { return Rect.zero; } + Offset localToGlobal(Offset offset) { + return _renderParagraph.localToGlobal(offset); + } + TextSpan get _decorateTextSpanWithGlobalStyle => TextSpan( children: _textSpan.children ?.whereType() diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 30b295765e..3bc3f5e0b5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -2,7 +2,6 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; From 883740d79a458a80a5431a4b9866bbf8fbc1b35d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 15:02:14 +0800 Subject: [PATCH 080/121] fix: assets of document --- frontend/app_flowy/packages/flowy_editor/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 08e51118d1..403ee2dddf 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -24,11 +24,11 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # To add assets to your package, add an assets section, like this: assets: - assets/images/uncheck.svg - assets/images/ + - assets/document.json # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # From 1d3e5a9e8b52a396a50c9ba96284416b728d0fb7 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 15:25:19 +0800 Subject: [PATCH 081/121] feat: handle shift keys --- .../arrow_keys_handler.dart | 97 ++++++++++++------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 3bc3f5e0b5..cec123ad87 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,7 +1,5 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/document/selection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,14 +10,65 @@ int _endOffsetOfNode(Node node) { return 0; } -FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.arrowUp && - event.logicalKey != LogicalKeyboardKey.arrowDown && - event.logicalKey != LogicalKeyboardKey.arrowLeft && - event.logicalKey != LogicalKeyboardKey.arrowRight) { +KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { + final currentSelection = editorState.cursorSelection; + if (currentSelection == null) { return KeyEventResult.ignored; } + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + final leftPosition = _leftPosition(editorState, currentSelection.start); + if (leftPosition != null) { + editorState.updateCursorSelection( + Selection(start: leftPosition, end: currentSelection.end)); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + final rightPosition = _rightPosition(editorState, currentSelection.end); + if (rightPosition != null) { + editorState.updateCursorSelection( + Selection(start: currentSelection.start, end: rightPosition)); + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +} + +Position? _leftPosition(EditorState editorState, Position position) { + final offset = position.offset; + if (offset == 0) { + final node = editorState.document.nodeAtPath(position.path)!; + final prevNode = node.previous; + if (prevNode != null) { + editorState.updateCursorSelection(Selection.collapsed( + Position(path: prevNode.path, offset: _endOffsetOfNode(prevNode)))); + } + return null; + } + + return Position(path: position.path, offset: offset - 1); +} + +Position? _rightPosition(EditorState editorState, Position position) { + final offset = position.offset; + final node = editorState.document.nodeAtPath(position.path)!; + final lengthOfNode = _endOffsetOfNode(node); + if (offset >= lengthOfNode) { + final nextNode = node.next; + if (nextNode != null) { + Position(path: nextNode.path, offset: 0); + } + return null; + } + + return Position(path: position.path, offset: offset + 1); +} + +FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { + if (event.isShiftPressed) { + return _handleShiftKey(editorState, event); + } + final currentSelection = editorState.cursorSelection; if (currentSelection == null) { return KeyEventResult.ignored; @@ -27,19 +76,10 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { if (currentSelection.isCollapsed) { - final end = currentSelection.end; - final offset = end.offset; - if (offset == 0) { - final node = editorState.document.nodeAtPath(end.path)!; - final prevNode = node.previous; - if (prevNode != null) { - editorState.updateCursorSelection(Selection.collapsed(Position( - path: prevNode.path, offset: _endOffsetOfNode(prevNode)))); - } - return KeyEventResult.handled; + final leftPosition = _leftPosition(editorState, currentSelection.start); + if (leftPosition != null) { + editorState.updateCursorSelection(Selection.collapsed(leftPosition)); } - editorState.updateCursorSelection( - Selection.collapsed(Position(path: end.path, offset: offset - 1))); } else { editorState .updateCursorSelection(currentSelection.collapse(atStart: true)); @@ -47,21 +87,10 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { if (currentSelection.isCollapsed) { - final end = currentSelection.end; - final offset = end.offset; - final node = editorState.document.nodeAtPath(end.path)!; - final lengthOfNode = _endOffsetOfNode(node); - if (offset >= lengthOfNode) { - final nextNode = node.next; - if (nextNode != null) { - editorState.updateCursorSelection( - Selection.collapsed(Position(path: nextNode.path, offset: 0))); - } - return KeyEventResult.handled; + final rightPosition = _rightPosition(editorState, currentSelection.end); + if (rightPosition != null) { + editorState.updateCursorSelection(Selection.collapsed(rightPosition)); } - - editorState.updateCursorSelection( - Selection.collapsed(Position(path: end.path, offset: offset + 1))); } else { editorState.updateCursorSelection(currentSelection.collapse()); } From b91c5d9c7b3e50b33d212723a53dbddd7ca6a51d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 15:33:42 +0800 Subject: [PATCH 082/121] refactor: add hitTest method for selection service --- .../arrow_keys_handler.dart | 138 ++++++++++-------- .../lib/service/selection_service.dart | 21 +-- 2 files changed, 92 insertions(+), 67 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index cec123ad87..7fbdf669b5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -10,6 +10,58 @@ int _endOffsetOfNode(Node node) { return 0; } +extension on Position { + Position? goLeft(EditorState editorState) { + if (offset == 0) { + final node = editorState.document.nodeAtPath(path)!; + final prevNode = node.previous; + if (prevNode != null) { + return Position( + path: prevNode.path, offset: _endOffsetOfNode(prevNode)); + } + return null; + } + + return Position(path: path, offset: offset - 1); + } + + Position? goRight(EditorState editorState) { + final node = editorState.document.nodeAtPath(path)!; + final lengthOfNode = _endOffsetOfNode(node); + if (offset >= lengthOfNode) { + final nextNode = node.next; + if (nextNode != null) { + return Position(path: nextNode.path, offset: 0); + } + return null; + } + + return Position(path: path, offset: offset + 1); + } +} + +Position? _goUp(EditorState editorState) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return null; + } + final first = rects.first; + final firstOffset = Offset(first.left, first.top); + final hitOffset = firstOffset - Offset(0, first.height * 0.5); + return editorState.service.selectionService.hitTest(hitOffset); +} + +Position? _goDown(EditorState editorState) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return null; + } + final first = rects.last; + final firstOffset = Offset(first.right, first.bottom); + final hitOffset = firstOffset + Offset(0, first.height * 0.5); + return editorState.service.selectionService.hitTest(hitOffset); +} + KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { final currentSelection = editorState.cursorSelection; if (currentSelection == null) { @@ -17,53 +69,33 @@ KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { } if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - final leftPosition = _leftPosition(editorState, currentSelection.start); - if (leftPosition != null) { - editorState.updateCursorSelection( - Selection(start: leftPosition, end: currentSelection.end)); - } + final leftPosition = currentSelection.end.goLeft(editorState); + editorState.updateCursorSelection(leftPosition == null + ? null + : Selection(start: currentSelection.start, end: leftPosition)); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - final rightPosition = _rightPosition(editorState, currentSelection.end); - if (rightPosition != null) { - editorState.updateCursorSelection( - Selection(start: currentSelection.start, end: rightPosition)); - } + final rightPosition = currentSelection.start.goRight(editorState); + editorState.updateCursorSelection(rightPosition == null + ? null + : Selection(start: rightPosition, end: currentSelection.end)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final position = _goUp(editorState); + editorState.updateCursorSelection(position == null + ? null + : Selection(start: position, end: currentSelection.end)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final position = _goDown(editorState); + editorState.updateCursorSelection(position == null + ? null + : Selection(start: currentSelection.start, end: position)); return KeyEventResult.handled; } return KeyEventResult.ignored; } -Position? _leftPosition(EditorState editorState, Position position) { - final offset = position.offset; - if (offset == 0) { - final node = editorState.document.nodeAtPath(position.path)!; - final prevNode = node.previous; - if (prevNode != null) { - editorState.updateCursorSelection(Selection.collapsed( - Position(path: prevNode.path, offset: _endOffsetOfNode(prevNode)))); - } - return null; - } - - return Position(path: position.path, offset: offset - 1); -} - -Position? _rightPosition(EditorState editorState, Position position) { - final offset = position.offset; - final node = editorState.document.nodeAtPath(position.path)!; - final lengthOfNode = _endOffsetOfNode(node); - if (offset >= lengthOfNode) { - final nextNode = node.next; - if (nextNode != null) { - Position(path: nextNode.path, offset: 0); - } - return null; - } - - return Position(path: position.path, offset: offset + 1); -} - FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { if (event.isShiftPressed) { return _handleShiftKey(editorState, event); @@ -76,7 +108,7 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { if (currentSelection.isCollapsed) { - final leftPosition = _leftPosition(editorState, currentSelection.start); + final leftPosition = currentSelection.start.goLeft(editorState); if (leftPosition != null) { editorState.updateCursorSelection(Selection.collapsed(leftPosition)); } @@ -87,7 +119,7 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { if (currentSelection.isCollapsed) { - final rightPosition = _rightPosition(editorState, currentSelection.end); + final rightPosition = currentSelection.end.goRight(editorState); if (rightPosition != null) { editorState.updateCursorSelection(Selection.collapsed(rightPosition)); } @@ -96,24 +128,14 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { } return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - final rects = editorState.service.selectionService.rects(); - if (rects.isEmpty) { - return KeyEventResult.handled; - } - final first = rects.first; - final firstOffset = Offset(first.left, first.top); - final hitOffset = firstOffset - Offset(0, first.height * 0.5); - editorState.service.selectionService.hit(hitOffset); + final position = _goUp(editorState); + editorState.updateCursorSelection( + position == null ? null : Selection.collapsed(position)); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - final rects = editorState.service.selectionService.rects(); - if (rects.isEmpty) { - return KeyEventResult.handled; - } - final first = rects.last; - final firstOffset = Offset(first.right, first.bottom); - final hitOffset = firstOffset + Offset(0, first.height * 0.5); - editorState.service.selectionService.hit(hitOffset); + final position = _goDown(editorState); + editorState.updateCursorSelection( + position == null ? null : Selection.collapsed(position)); return KeyEventResult.handled; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index c585c13bdd..3cfd1fd3f7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -30,7 +30,7 @@ mixin FlowySelectionService on State { List rects(); - hit(Offset? offset); + Position? hitTest(Offset? offset); /// List getNodesInSelection(Selection selection); @@ -285,29 +285,32 @@ class _FlowySelectionState extends State tapOffset = details.globalPosition; - hit(tapOffset); + final position = hitTest(tapOffset); + if (position == null) { + return; + } + final selection = Selection.collapsed(position); + editorState.updateCursorSelection(selection); } @override - hit(Offset? offset) { + Position? hitTest(Offset? offset) { if (offset == null) { editorState.updateCursorSelection(null); - return; + return null; } final nodes = getNodesInRange(offset); if (nodes.isEmpty) { editorState.updateCursorSelection(null); - return; + return null; } assert(nodes.length == 1); final selectable = nodes.first.selectable; if (selectable == null) { editorState.updateCursorSelection(null); - return; + return null; } - final position = selectable.getPositionInOffset(offset); - final selection = Selection.collapsed(position); - editorState.updateCursorSelection(selection); + return selectable.getPositionInOffset(offset); } void _onPanStart(DragStartDetails details) { From 2a09f69bec069cec085db61b99c6c3c04ba0121e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 18:06:54 +0800 Subject: [PATCH 083/121] feat: double tap on text --- .../lib/render/rich_text/flowy_rich_text.dart | 10 ++ .../lib/render/selection/selectable.dart | 4 + .../lib/service/selection_service.dart | 133 +++++++++++++++--- 3 files changed, 125 insertions(+), 22 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 4731542ae2..122b65991e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -108,6 +108,16 @@ class _FlowyRichTextState extends State with Selectable { return Position(path: _textNode.path, offset: baseOffset); } + @override + Selection? getWorldBoundaryInOffset(Offset offset) { + final localOffset = _renderParagraph.globalToLocal(offset); + final textPosition = _renderParagraph.getPositionForOffset(localOffset); + final textRange = _renderParagraph.getWordBoundary(textPosition); + final start = Position(path: _textNode.path, offset: textRange.start); + final end = Position(path: _textNode.path, offset: textRange.end); + return Selection(start: start, end: end); + } + @override List getRectsInSelection(Selection selection) { assert(pathEquals(selection.start.path, selection.end.path) && diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index b677b2f47c..bc32706aa0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -21,6 +21,10 @@ mixin Selectable on State { /// /// The return result must be an offset of the local coordinate system. Position getPositionInOffset(Offset start); + Selection? getWorldBoundaryInOffset(Offset start) { + return null; + } + Rect getCursorRectInPosition(Position position); Offset localToGlobal(Offset offset); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 3cfd1fd3f7..43b77baeaf 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; @@ -6,10 +8,10 @@ import 'package:flowy_editor/render/selection/cursor_widget.dart'; import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flutter/gestures.dart'; import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flowy_editor/editor_state.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; /// Process selection and cursor @@ -99,6 +101,92 @@ class FlowySelection extends StatefulWidget { State createState() => _FlowySelectionState(); } +/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer] +/// for a while. So we need to implement our own GestureDetector. +@immutable +class _SelectionGestureDetector extends StatefulWidget { + const _SelectionGestureDetector( + {Key? key, + this.child, + this.onTapDown, + this.onDoubleTapDown, + this.onPanStart, + this.onPanUpdate, + this.onPanEnd}) + : super(key: key); + + @override + State<_SelectionGestureDetector> createState() => + _SelectionGestureDetectorState(); + + final Widget? child; + + final GestureTapDownCallback? onTapDown; + final GestureTapDownCallback? onDoubleTapDown; + final GestureDragStartCallback? onPanStart; + final GestureDragUpdateCallback? onPanUpdate; + final GestureDragEndCallback? onPanEnd; +} + +class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> { + bool _isDoubleTap = false; + Timer? _doubleTapTimer; + @override + Widget build(BuildContext context) { + return RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: { + PanGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => PanGestureRecognizer(), + (recognizer) { + recognizer + ..onStart = widget.onPanStart + ..onUpdate = widget.onPanUpdate + ..onEnd = widget.onPanEnd; + }, + ), + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (recognizer) { + recognizer.onTapDown = _tapDownDelegate; + }, + ), + }, + child: widget.child, + ); + } + + _tapDownDelegate(TapDownDetails tapDownDetails) { + if (_isDoubleTap) { + _isDoubleTap = false; + _doubleTapTimer?.cancel(); + _doubleTapTimer = null; + if (widget.onDoubleTapDown != null) { + widget.onDoubleTapDown!(tapDownDetails); + } + } else { + if (widget.onTapDown != null) { + widget.onTapDown!(tapDownDetails); + } + + _isDoubleTap = true; + _doubleTapTimer?.cancel(); + _doubleTapTimer = Timer(kDoubleTapTimeout, () { + _isDoubleTap = false; + _doubleTapTimer = null; + }); + } + } + + @override + void dispose() { + _doubleTapTimer?.cancel(); + super.dispose(); + } +} + class _FlowySelectionState extends State with FlowySelectionService, WidgetsBindingObserver { final _cursorKey = GlobalKey(debugLabel: 'cursor'); @@ -152,27 +240,12 @@ class _FlowySelectionState extends State @override Widget build(BuildContext context) { - return RawGestureDetector( - behavior: HitTestBehavior.translucent, - gestures: { - PanGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => PanGestureRecognizer(), - (recognizer) { - recognizer - ..onStart = _onPanStart - ..onUpdate = _onPanUpdate - ..onEnd = _onPanEnd; - }, - ), - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), - (recognizer) { - recognizer.onTapDown = _onTapDown; - }, - ) - }, + return _SelectionGestureDetector( + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + onPanEnd: _onPanEnd, + onTapDown: _onTapDown, + onDoubleTapDown: _onDoubleTapDown, child: widget.child, ); } @@ -278,6 +351,22 @@ class _FlowySelectionState extends State return false; } + void _onDoubleTapDown(TapDownDetails details) { + final offset = details.globalPosition; + final nodes = getNodesInRange(offset); + if (nodes.isEmpty) { + editorState.updateCursorSelection(null); + return; + } + final selectable = nodes.first.selectable; + if (selectable == null) { + editorState.updateCursorSelection(null); + return; + } + editorState + .updateCursorSelection(selectable.getWorldBoundaryInOffset(offset)); + } + void _onTapDown(TapDownDetails details) { // clear old state. panStartOffset = null; From 734b642fcc0e05a49d86c99c87dc8f6c44a48d0c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 28 Jul 2022 19:40:13 +0800 Subject: [PATCH 084/121] feat: add checkbox and heading style --- .../flowy_editor/example/assets/document.json | 2 +- .../flowy_editor/example/assets/example.json | 62 ++++++++ .../flowy_editor/lib/editor_state.dart | 4 + .../lib/render/rich_text/checkbox_text.dart | 132 ++++++++++++++++++ .../render/rich_text/default_selectable.dart | 28 ++++ .../lib/render/rich_text/flowy_rich_text.dart | 24 ++-- .../lib/render/rich_text/heading_text.dart | 97 +++++++++++++ .../lib/render/rich_text/rich_text_style.dart | 12 +- .../lib/service/editor_service.dart | 2 +- .../lib/service/selection_service.dart | 2 +- 10 files changed, 349 insertions(+), 16 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index b90aec8369..307b4bf92f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -68,7 +68,7 @@ { "insert": " your ", "attributes": { "bold": true } }, { "insert": "writing", "attributes": { "underline": true } }, { - "insert": " howeverv you like.", + "insert": " however you like.", "attributes": { "strikethrough": true } } ], diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 0dad009cd2..d482ab2450 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -17,6 +17,7 @@ } ], "attributes": { + "subtype": "heading", "heading": "h1" } }, @@ -28,9 +29,68 @@ } ], "attributes": { + "subtype": "heading", "heading": "h2" } }, + { + "type": "text", + "delta": [ + { + "insert": "Here are the plugin demos:" + } + ], + "attributes": { + "subtype": "heading", + "heading": "h3" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Checkbox example ......" + } + ], + "attributes": { + "subtype": "checkbox", + "checkbox": false + }, + "children": [ + { + "type": "text", + "delta": [ + { + "insert": "AAA Checkbox example ......\nAAA Checkbox example ......" + } + ], + "attributes": { + "subtype": "checkbox", + "checkbox": false + } + }, + { + "type": "text", + "delta": [ + { + "insert": "BBB Checkbox example ......" + } + ], + "attributes": { + "subtype": "checkbox", + "checkbox": true + } + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "Raw text example ......" + } + ] + }, { "type": "text", "delta": [ @@ -39,6 +99,7 @@ } ], "attributes": { + "subtype": "heading", "heading": "h3" } }, @@ -97,6 +158,7 @@ } ], "attributes": { + "subtype": "heading", "heading": "h3" } }, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index e388ea3661..6bc07078d3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/service/service.dart'; import 'package:flutter/material.dart'; @@ -43,6 +45,8 @@ class EditorState { }) { // FIXME: abstract render plugins as a service. renderPlugins.register('text', RichTextNodeWidgetBuilder.create); + renderPlugins.register('text/checkbox', CheckboxNodeWidgetBuilder.create); + renderPlugins.register('text/heading', HeadingTextNodeWidgetBuilder.create); undoManager.state = this; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart new file mode 100644 index 0000000000..11f1c9bba1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -0,0 +1,132 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/node_widget_builder.dart'; +import 'package:flowy_editor/render/render_plugins.dart'; +import 'package:flowy_editor/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flutter/material.dart'; + +class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder { + CheckboxNodeWidgetBuilder.create({ + required super.editorState, + required super.node, + required super.key, + }) : super.create(); + + @override + Widget build(BuildContext context) { + return CheckboxNodeWidget( + key: key, + textNode: node as TextNode, + editorState: editorState, + ); + } +} + +class CheckboxNodeWidget extends StatefulWidget { + const CheckboxNodeWidget({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State createState() => _CheckboxNodeWidgetState(); +} + +class _CheckboxNodeWidgetState extends State + with Selectable, DefaultSelectable { + final _checkboxKey = GlobalKey(debugLabel: 'checkbox'); + final _richTextKey = GlobalKey(debugLabel: 'checkbox_text'); + + @override + Selectable get forward => + _richTextKey.currentState as Selectable; + + @override + Offset get baseOffset { + final width = _checkboxKey.currentContext + ?.findRenderObject() + ?.unwrapOrNull() + ?.size + .width; + if (width != null) { + return Offset(width, 0); + } + return Offset.zero; + } + + @override + Widget build(BuildContext context) { + if (widget.textNode.children.isEmpty) { + return _buildWithSingle(context); + } else { + return _buildWithChildren(context); + } + } + + Widget _buildWithSingle(BuildContext context) { + final check = widget.textNode.attributes.checkbox; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + child: FlowySvg( + key: _checkboxKey, + name: check ? 'check' : 'uncheck', + ), + onTap: () { + debugPrint('[Checkbox] onTap...'); + TransactionBuilder(widget.editorState) + ..updateNode(widget.textNode, { + 'checkbox': !check, + }) + ..commit(); + }, + ), + FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + ) + ], + ); + } + + Widget _buildWithChildren(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildWithSingle(context), + Row( + children: [ + const SizedBox( + width: 20, + ), + Column( + children: widget.textNode.children + .map( + (child) => widget.editorState.renderPlugins.buildWidget( + context: NodeWidgetContext( + buildContext: context, + node: child, + editorState: widget.editorState, + ), + ), + ) + .toList(), + ) + ], + ) + ], + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart new file mode 100644 index 0000000000..fe8ba39730 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart @@ -0,0 +1,28 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flutter/material.dart'; + +mixin DefaultSelectable { + Selectable get forward; + + Offset get baseOffset; + + Position getPositionInOffset(Offset start) => + forward.getPositionInOffset(start); + + Rect getCursorRectInPosition(Position position) => + forward.getCursorRectInPosition(position).shift(baseOffset); + + List getRectsInSelection(Selection selection) => forward + .getRectsInSelection(selection) + .map((rect) => rect.shift(baseOffset)) + .toList(growable: false); + + Selection getSelectionInRange(Offset start, Offset end) => + forward.getSelectionInRange(start, end); + + Position start() => forward.start(); + + Position end() => forward.end(); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 66c87a2dd4..33b1ece074 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -32,11 +32,14 @@ class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { } } +typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); + class FlowyRichText extends StatefulWidget { const FlowyRichText({ Key? key, this.cursorHeight, this.cursorWidth = 2.0, + this.textSpanDecorator, required this.textNode, required this.editorState, }) : super(key: key); @@ -45,6 +48,7 @@ class FlowyRichText extends StatefulWidget { final double cursorWidth; final TextNode textNode; final EditorState editorState; + final FlowyTextSpanDecorator? textSpanDecorator; @override State createState() => _FlowyRichTextState(); @@ -70,7 +74,7 @@ class _FlowyRichTextState extends State with Selectable { } else if (attributes.quote == true) { return _buildQuotedRichText(context); } else if (attributes.heading != null) { - return _buildHeadingRichText(context); + // return _buildHeadingRichText(context); } else if (attributes.number != null) { return _buildNumberListRichText(context); } @@ -87,14 +91,13 @@ class _FlowyRichTextState extends State with Selectable { @override Rect getCursorRectInPosition(Position position) { final textPosition = TextPosition(offset: position.offset); - final baseRect = frontWidgetRect(); final cursorOffset = _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); final cursorHeight = widget.cursorHeight ?? _renderParagraph.getFullHeightForCaret(textPosition) ?? 5.0; // default height return Rect.fromLTWH( - baseRect.centerRight.dx + cursorOffset.dx - (widget.cursorWidth / 2), + cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, widget.cursorWidth, cursorHeight, @@ -138,11 +141,7 @@ class _FlowyRichTextState extends State with Selectable { } Widget _buildRichText(BuildContext context) { - if (_textNode.children.isEmpty) { - return _buildSingleRichText(context); - } else { - return _buildRichTextWithChildren(context); - } + return _buildSingleRichText(context); } Widget _buildRichTextWithChildren(BuildContext context) { @@ -166,10 +165,11 @@ class _FlowyRichTextState extends State with Selectable { } Widget _buildSingleRichText(BuildContext context) { - return SizedBox( - width: - MediaQuery.of(context).size.width - 20, // FIXME: use the const value - child: RichText(key: _textKey, text: _decorateTextSpanWithGlobalStyle), + return RichText( + key: _textKey, + text: widget.textSpanDecorator != null + ? widget.textSpanDecorator!(_decorateTextSpanWithGlobalStyle) + : _decorateTextSpanWithGlobalStyle, ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart new file mode 100644 index 0000000000..ac4746b3e4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart @@ -0,0 +1,97 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/render/node_widget_builder.dart'; +import 'package:flowy_editor/render/render_plugins.dart'; +import 'package:flowy_editor/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flutter/material.dart'; + +class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder { + HeadingTextNodeWidgetBuilder.create({ + required super.editorState, + required super.node, + required super.key, + }) : super.create(); + + @override + Widget build(BuildContext context) { + return HeadingTextNodeWidget( + key: key, + textNode: node as TextNode, + editorState: editorState, + ); + } +} + +class HeadingTextNodeWidget extends StatefulWidget { + const HeadingTextNodeWidget({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State createState() => _HeadingTextNodeWidgetState(); +} + +// customize + +class _HeadingTextNodeWidgetState extends State + with Selectable, DefaultSelectable { + final _richTextKey = GlobalKey(debugLabel: 'heading_text'); + final topPadding = 5.0; + final bottomPadding = 2.0; + + @override + Selectable get forward => + _richTextKey.currentState as Selectable; + + @override + Offset get baseOffset { + return Offset(0, topPadding); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: topPadding, + ), + FlowyRichText( + key: _richTextKey, + textSpanDecorator: _textSpanDecorator, + textNode: widget.textNode, + editorState: widget.editorState, + ), + SizedBox( + height: bottomPadding, + ), + ], + ); + } + + TextSpan _textSpanDecorator(TextSpan textSpan) { + return TextSpan( + children: textSpan.children + ?.whereType() + .map( + (span) => TextSpan( + text: span.text, + style: span.style?.copyWith( + fontSize: widget.textNode.attributes.fontSize, + ), + recognizer: span.recognizer, + ), + ) + .toList(), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index b4100f9b87..3c11feb20d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -25,12 +25,15 @@ class StyleKey { static String font = 'font'; static String href = 'href'; - static String heading = 'heading'; static String quote = 'quote'; static String list = 'list'; static String number = 'number'; static String todo = 'todo'; static String code = 'code'; + + static String subtype = 'subtype'; + static String checkbox = 'checkbox'; + static String heading = 'heading'; } double baseFontSize = 16.0; @@ -100,6 +103,13 @@ extension NodeAttributesExtensions on Attributes { } return false; } + + bool get checkbox { + if (containsKey(StyleKey.checkbox) && this[StyleKey.checkbox] is bool) { + return this[StyleKey.checkbox]; + } + return false; + } } extension DeltaAttributesExtensions on Attributes { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index d5223ec36a..68f08ac4ce 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -23,7 +23,7 @@ class FlowyEditor extends StatefulWidget { final EditorState editorState; final List keyEventHandlers; - /// Shortcusts + /// shortcuts final FloatingShortcuts shortcuts; @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 07cf2ad902..878fac28b4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -165,7 +165,7 @@ class _FlowySelectionState extends State (recognizer) { recognizer.onTapDown = _onTapDown; }, - ) + ), }, child: widget.child, ); From 51bc965029f96c966f6d8c1cb84f68c86b486cd0 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 28 Jul 2022 19:59:15 +0800 Subject: [PATCH 085/121] feat: add bulleted-list and number-list --- .../flowy_editor/example/assets/example.json | 21 +++--- .../flowy_editor/lib/editor_state.dart | 6 ++ .../render/rich_text/bulleted_list_text.dart | 73 ++++++++++++++++++ .../lib/render/rich_text/flowy_rich_text.dart | 6 +- .../lib/render/rich_text/heading_text.dart | 3 - .../render/rich_text/number_list_text.dart | 74 +++++++++++++++++++ 6 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index d482ab2450..2fb4a4d8a0 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -111,8 +111,8 @@ { "insert": " and just typing." } ], "attributes": { - "list": "todo", - "todo": true + "subtype": "checkbox", + "checkbox": true } }, { @@ -130,8 +130,8 @@ } ], "attributes": { - "list": "todo", - "todo": true + "subtype": "checkbox", + "checkbox": true } }, { @@ -146,8 +146,8 @@ { "insert": "." } ], "attributes": { - "list": "todo", - "todo": true + "subtype": "checkbox", + "checkbox": true } }, { @@ -170,7 +170,7 @@ } ], "attributes": { - "list": "bullet" + "subtype": "bullet-list" } }, { @@ -181,7 +181,7 @@ } ], "attributes": { - "list": "bullet" + "subtype": "bullet-list" } }, { @@ -192,7 +192,7 @@ } ], "attributes": { - "list": "bullet" + "subtype": "bullet-list" } }, { @@ -225,6 +225,7 @@ } ], "attributes": { + "subtype": "number-list", "number": 1 } }, @@ -236,6 +237,7 @@ } ], "attributes": { + "subtype": "number-list", "number": 2 } }, @@ -247,6 +249,7 @@ } ], "attributes": { + "subtype": "number-list", "number": 3 } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 6bc07078d3..a417d80513 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; +import 'package:flowy_editor/render/rich_text/number_list_text.dart'; import 'package:flowy_editor/service/service.dart'; import 'package:flutter/material.dart'; @@ -47,6 +49,10 @@ class EditorState { renderPlugins.register('text', RichTextNodeWidgetBuilder.create); renderPlugins.register('text/checkbox', CheckboxNodeWidgetBuilder.create); renderPlugins.register('text/heading', HeadingTextNodeWidgetBuilder.create); + renderPlugins.register( + 'text/bullet-list', BulletedListTextNodeWidgetBuilder.create); + renderPlugins.register( + 'text/number-list', NumberListTextNodeWidgetBuilder.create); undoManager.state = this; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart new file mode 100644 index 0000000000..faf1cb8c56 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart @@ -0,0 +1,73 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/render/node_widget_builder.dart'; +import 'package:flowy_editor/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flutter/material.dart'; + +class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder { + BulletedListTextNodeWidgetBuilder.create({ + required super.editorState, + required super.node, + required super.key, + }) : super.create(); + + @override + Widget build(BuildContext context) { + return BulletedListTextNodeWidget( + key: key, + textNode: node as TextNode, + editorState: editorState, + ); + } +} + +class BulletedListTextNodeWidget extends StatefulWidget { + const BulletedListTextNodeWidget({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State createState() => + _BulletedListTextNodeWidgetState(); +} + +// customize + +class _BulletedListTextNodeWidgetState extends State + with Selectable, DefaultSelectable { + final _richTextKey = GlobalKey(debugLabel: 'heading_text'); + final leftPadding = 20.0; + + @override + Selectable get forward => + _richTextKey.currentState as Selectable; + + @override + Offset get baseOffset { + return Offset(leftPadding, 0); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const FlowySvg( + name: 'point', + ), + FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + ), + ], + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 33b1ece074..0891ee72cf 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -68,15 +68,15 @@ class _FlowyRichTextState extends State with Selectable { final attributes = _textNode.attributes; // TODO: use factory method ?? if (attributes.list == 'todo') { - return _buildTodoListRichText(context); + // return _buildTodoListRichText(context); } else if (attributes.list == 'bullet') { - return _buildBulletedListRichText(context); + // return _buildBulletedListRichText(context); } else if (attributes.quote == true) { return _buildQuotedRichText(context); } else if (attributes.heading != null) { // return _buildHeadingRichText(context); } else if (attributes.number != null) { - return _buildNumberListRichText(context); + // return _buildNumberListRichText(context); } return _buildRichText(context); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart index ac4746b3e4..3d2ac14756 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart @@ -1,13 +1,10 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; import 'package:flowy_editor/render/node_widget_builder.dart'; -import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flowy_editor/render/rich_text/default_selectable.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flutter/material.dart'; class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart new file mode 100644 index 0000000000..082be930e9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart @@ -0,0 +1,74 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/render/node_widget_builder.dart'; +import 'package:flowy_editor/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flutter/material.dart'; + +class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder { + NumberListTextNodeWidgetBuilder.create({ + required super.editorState, + required super.node, + required super.key, + }) : super.create(); + + @override + Widget build(BuildContext context) { + return NumberListTextNodeWidget( + key: key, + textNode: node as TextNode, + editorState: editorState, + ); + } +} + +class NumberListTextNodeWidget extends StatefulWidget { + const NumberListTextNodeWidget({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State createState() => + _NumberListTextNodeWidgetState(); +} + +// customize + +class _NumberListTextNodeWidgetState extends State + with Selectable, DefaultSelectable { + final _richTextKey = GlobalKey(debugLabel: 'heading_text'); + final leftPadding = 20.0; + + @override + Selectable get forward => + _richTextKey.currentState as Selectable; + + @override + Offset get baseOffset { + return Offset(leftPadding, 0); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + FlowySvg( + number: widget.textNode.attributes.number, + ), + FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + ), + ], + ); + } +} From fce8ea1e8067ab2c68b9854181e1414cb0e067d7 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 28 Jul 2022 23:04:51 +0800 Subject: [PATCH 086/121] feat: add checkbox style --- .../flowy_editor/example/assets/example.json | 125 ++++++------- .../example/lib/plugin/image_node_widget.dart | 19 +- .../flowy_editor/lib/editor_state.dart | 2 + .../lib/render/rich_text/flowy_rich_text.dart | 166 +++--------------- .../lib/render/rich_text/quoted_text.dart | 73 ++++++++ 5 files changed, 157 insertions(+), 228 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 2fb4a4d8a0..6a0fba3021 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -6,7 +6,7 @@ { "type": "image", "attributes": { - "image_src": "https://images.pexels.com/photos/2253275/pexels-photo-2253275.jpeg?cs=srgb&dl=pexels-helena-lopes-2253275.jpg&fm=jpg" + "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" } }, { @@ -37,57 +37,7 @@ "type": "text", "delta": [ { - "insert": "Here are the plugin demos:" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h3" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Checkbox example ......" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - }, - "children": [ - { - "type": "text", - "delta": [ - { - "insert": "AAA Checkbox example ......\nAAA Checkbox example ......" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { - "insert": "BBB Checkbox example ......" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": true - } - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Raw text example ......" + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." } ] }, @@ -109,11 +59,7 @@ { "insert": "Click " }, { "insert": "anywhere", "attributes": { "underline": true } }, { "insert": " and just typing." } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": true - } + ] }, { "type": "text", @@ -128,11 +74,7 @@ { "insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc." } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": true - } + ] }, { "type": "text", @@ -144,17 +86,13 @@ { "insert": " your ", "attributes": { "italic": true } }, { "insert": "writing", "attributes": { "strikethrough": true } }, { "insert": "." } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": true - } + ] }, { "type": "text", "delta": [ { - "insert": "Here are the examples:" + "insert": "Here are the plugins:" } ], "attributes": { @@ -162,6 +100,42 @@ "heading": "h3" } }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "subtype": "checkbox", + "checkbox": false + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "subtype": "checkbox", + "checkbox": false + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "subtype": "checkbox", + "checkbox": false + } + }, { "type": "text", "delta": [ @@ -203,7 +177,7 @@ } ], "attributes": { - "quote": true + "subtype": "quote" } }, { @@ -214,7 +188,18 @@ } ], "attributes": { - "quote": true + "subtype": "quote" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "subtype": "quote" } }, { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index e33ff83e2f..a57f41cf76 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -85,23 +85,8 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { children: [ Image.network( src, - height: 150.0, - ), - if (node.children.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: e, - editorState: editorState, - ), - ), - ) - .toList(), - ), + width: MediaQuery.of(context).size.width, + ) ], ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index a417d80513..82ffc38e6a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -4,6 +4,7 @@ import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/render/rich_text/number_list_text.dart'; +import 'package:flowy_editor/render/rich_text/quoted_text.dart'; import 'package:flowy_editor/service/service.dart'; import 'package:flutter/material.dart'; @@ -53,6 +54,7 @@ class EditorState { 'text/bullet-list', BulletedListTextNodeWidgetBuilder.create); renderPlugins.register( 'text/number-list', NumberListTextNodeWidgetBuilder.create); + renderPlugins.register('text/quote', QuotedTextNodeWidgetBuilder.create); undoManager.state = this; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 0891ee72cf..89ebce3026 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -4,12 +4,9 @@ import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/render/node_widget_builder.dart'; import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flutter/material.dart'; @@ -56,37 +53,21 @@ class FlowyRichText extends StatefulWidget { class _FlowyRichTextState extends State with Selectable { final _textKey = GlobalKey(); - final _decorationKey = GlobalKey(); - EditorState get _editorState => widget.editorState; - TextNode get _textNode => widget.textNode; RenderParagraph get _renderParagraph => _textKey.currentContext?.findRenderObject() as RenderParagraph; @override Widget build(BuildContext context) { - final attributes = _textNode.attributes; - // TODO: use factory method ?? - if (attributes.list == 'todo') { - // return _buildTodoListRichText(context); - } else if (attributes.list == 'bullet') { - // return _buildBulletedListRichText(context); - } else if (attributes.quote == true) { - return _buildQuotedRichText(context); - } else if (attributes.heading != null) { - // return _buildHeadingRichText(context); - } else if (attributes.number != null) { - // return _buildNumberListRichText(context); - } return _buildRichText(context); } @override - Position start() => Position(path: _textNode.path, offset: 0); + Position start() => Position(path: widget.textNode.path, offset: 0); @override - Position end() => - Position(path: _textNode.path, offset: _textNode.toRawString().length); + Position end() => Position( + path: widget.textNode.path, offset: widget.textNode.toRawString().length); @override Rect getCursorRectInPosition(Position position) { @@ -108,23 +89,22 @@ class _FlowyRichTextState extends State with Selectable { Position getPositionInOffset(Offset start) { final offset = _renderParagraph.globalToLocal(start); final baseOffset = _renderParagraph.getPositionForOffset(offset).offset; - return Position(path: _textNode.path, offset: baseOffset); + return Position(path: widget.textNode.path, offset: baseOffset); } @override List getRectsInSelection(Selection selection) { assert(pathEquals(selection.start.path, selection.end.path) && - pathEquals(selection.start.path, _textNode.path)); + pathEquals(selection.start.path, widget.textNode.path)); final textSelection = TextSelection( baseOffset: selection.start.offset, extentOffset: selection.end.offset, ); - final baseRect = frontWidgetRect(); - return _renderParagraph.getBoxesForSelection(textSelection).map((box) { - final rect = box.toRect(); - return rect.translate(baseRect.centerRight.dx, 0); - }).toList(); + return _renderParagraph + .getBoxesForSelection(textSelection) + .map((box) => box.toRect()) + .toList(); } @override @@ -134,7 +114,7 @@ class _FlowyRichTextState extends State with Selectable { final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset; final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset; return Selection.single( - path: _textNode.path, + path: widget.textNode.path, startOffset: baseOffset, endOffset: extentOffset, ); @@ -144,18 +124,29 @@ class _FlowyRichTextState extends State with Selectable { return _buildSingleRichText(context); } + Widget _buildSingleRichText(BuildContext context) { + final textSpan = _textSpan; + return RichText( + key: _textKey, + text: widget.textSpanDecorator != null + ? widget.textSpanDecorator!(textSpan) + : textSpan, + ); + } + + // unused now. Widget _buildRichTextWithChildren(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSingleRichText(context), - ..._textNode.children + ...widget.textNode.children .map( - (child) => _editorState.renderPlugins.buildWidget( + (child) => widget.editorState.renderPlugins.buildWidget( context: NodeWidgetContext( buildContext: context, node: child, - editorState: _editorState, + editorState: widget.editorState, ), ), ) @@ -164,115 +155,8 @@ class _FlowyRichTextState extends State with Selectable { ); } - Widget _buildSingleRichText(BuildContext context) { - return RichText( - key: _textKey, - text: widget.textSpanDecorator != null - ? widget.textSpanDecorator!(_decorateTextSpanWithGlobalStyle) - : _decorateTextSpanWithGlobalStyle, - ); - } - - Widget _buildTodoListRichText(BuildContext context) { - final name = _textNode.attributes.todo ? 'check' : 'uncheck'; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - child: FlowySvg( - key: _decorationKey, - name: name, - ), - onTap: () => TransactionBuilder(_editorState) - ..updateNode(_textNode, { - 'todo': !_textNode.attributes.todo, - }) - ..commit(), - ), - _buildRichText(context), - ], - ); - } - - Widget _buildBulletedListRichText(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FlowySvg( - key: _decorationKey, - name: 'point', - ), - _buildRichText(context), - ], - ); - } - - Widget _buildNumberListRichText(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FlowySvg( - key: _decorationKey, - number: _textNode.attributes.number, - ), - _buildRichText(context), - ], - ); - } - - Widget _buildQuotedRichText(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - key: _decorationKey, - name: 'quote', - ), - _buildRichText(context), - ], - ); - } - - Widget _buildHeadingRichText(BuildContext context) { - // TODO: customize - return Column( - children: [ - const Padding(padding: EdgeInsets.only(top: 5)), - _buildRichText(context), - const Padding(padding: EdgeInsets.only(top: 5)), - ], - ); - } - - Rect frontWidgetRect() { - // FIXME: find a more elegant way to solve this situation. - final renderBox = _decorationKey.currentContext - ?.findRenderObject() - ?.unwrapOrNull(); - if (renderBox != null) { - return renderBox.localToGlobal(Offset.zero) & renderBox.size; - } - return Rect.zero; - } - - TextSpan get _decorateTextSpanWithGlobalStyle => TextSpan( - children: _textSpan.children - ?.whereType() - .map( - (span) => TextSpan( - text: span.text, - style: span.style?.copyWith( - fontSize: _textNode.attributes.fontSize, - color: _textNode.attributes.quoteColor, - ), - recognizer: span.recognizer, - ), - ) - .toList(), - ); - TextSpan get _textSpan => TextSpan( - children: _textNode.delta.operations + children: widget.textNode.delta.operations .whereType() .map((insert) => RichTextStyle( attributes: insert.attributes ?? {}, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart new file mode 100644 index 0000000000..773fd0debe --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart @@ -0,0 +1,73 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/render/node_widget_builder.dart'; +import 'package:flowy_editor/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flutter/material.dart'; + +class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder { + QuotedTextNodeWidgetBuilder.create({ + required super.editorState, + required super.node, + required super.key, + }) : super.create(); + + @override + Widget build(BuildContext context) { + return QuotedTextNodeWidget( + key: key, + textNode: node as TextNode, + editorState: editorState, + ); + } +} + +class QuotedTextNodeWidget extends StatefulWidget { + const QuotedTextNodeWidget({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State createState() => _QuotedTextNodeWidgetState(); +} + +// customize + +class _QuotedTextNodeWidgetState extends State + with Selectable, DefaultSelectable { + final _richTextKey = GlobalKey(debugLabel: 'quoted_text'); + final leftPadding = 20.0; + + @override + Selectable get forward => + _richTextKey.currentState as Selectable; + + @override + Offset get baseOffset { + return Offset(leftPadding, 0); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const FlowySvg( + name: 'quote', + ), + FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + ), + ], + ); + } +} From 7489455c20403977281c7d7a443edc4daad6af94 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 10:34:59 +0800 Subject: [PATCH 087/121] chore: typo --- .../flowy_editor/lib/render/rich_text/number_list_text.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart index 082be930e9..368272f2d2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart @@ -44,7 +44,7 @@ class NumberListTextNodeWidget extends StatefulWidget { class _NumberListTextNodeWidgetState extends State with Selectable, DefaultSelectable { - final _richTextKey = GlobalKey(debugLabel: 'heading_text'); + final _richTextKey = GlobalKey(debugLabel: 'number_list_text'); final leftPadding = 20.0; @override From e4c3a3ae84bc83a00d31ad5cde300fd99e1b17c9 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 10:38:02 +0800 Subject: [PATCH 088/121] feat: modify svg size --- .../lib/render/rich_text/bulleted_list_text.dart | 5 +++-- .../lib/render/rich_text/checkbox_text.dart | 15 ++++----------- .../lib/render/rich_text/number_list_text.dart | 1 + .../lib/render/rich_text/quoted_text.dart | 3 ++- .../flowy_editor/lib/service/input_service.dart | 2 +- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart index faf1cb8c56..a345f7de3f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart @@ -43,7 +43,7 @@ class BulletedListTextNodeWidget extends StatefulWidget { class _BulletedListTextNodeWidgetState extends State with Selectable, DefaultSelectable { - final _richTextKey = GlobalKey(debugLabel: 'heading_text'); + final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text'); final leftPadding = 20.0; @override @@ -59,7 +59,8 @@ class _BulletedListTextNodeWidgetState extends State Widget build(BuildContext context) { return Row( children: [ - const FlowySvg( + FlowySvg( + size: Size.square(leftPadding), name: 'point', ), FlowyRichText( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index 11f1c9bba1..cdadcfe08c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -44,24 +44,17 @@ class CheckboxNodeWidget extends StatefulWidget { class _CheckboxNodeWidgetState extends State with Selectable, DefaultSelectable { - final _checkboxKey = GlobalKey(debugLabel: 'checkbox'); final _richTextKey = GlobalKey(debugLabel: 'checkbox_text'); + final leftPadding = 20.0; + @override Selectable get forward => _richTextKey.currentState as Selectable; @override Offset get baseOffset { - final width = _checkboxKey.currentContext - ?.findRenderObject() - ?.unwrapOrNull() - ?.size - .width; - if (width != null) { - return Offset(width, 0); - } - return Offset.zero; + return Offset(leftPadding, 0); } @override @@ -80,7 +73,7 @@ class _CheckboxNodeWidgetState extends State children: [ GestureDetector( child: FlowySvg( - key: _checkboxKey, + size: Size.square(leftPadding), name: check ? 'check' : 'uncheck', ), onTap: () { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart index 368272f2d2..288199f797 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart @@ -61,6 +61,7 @@ class _NumberListTextNodeWidgetState extends State return Row( children: [ FlowySvg( + size: Size.square(leftPadding), number: widget.textNode.attributes.number, ), FlowyRichText( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart index b4ee999ce8..8d0c919d7b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart @@ -58,7 +58,8 @@ class _QuotedTextNodeWidgetState extends State Widget build(BuildContext context) { return Row( children: [ - const FlowySvg( + FlowySvg( + size: Size.square(leftPadding), name: 'quote', ), FlowyRichText( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart index bdbcd24467..38309414f4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -90,7 +90,7 @@ class _FlowyInputState extends State @override void apply(List deltas) { -// TODO: implement the detail + // TODO: implement the detail for (final delta in deltas) { if (delta is TextEditingDeltaInsertion) { } else if (delta is TextEditingDeltaDeletion) { From c5e9008f4b1f68b053a466b35fe490fe7dd1ba1d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 10:41:39 +0800 Subject: [PATCH 089/121] feat: wrapping heading text with Padding instead of two SizeBoxs. --- .../lib/render/rich_text/heading_text.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart index 3d2ac14756..0453ac6952 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart @@ -59,18 +59,18 @@ class _HeadingTextNodeWidgetState extends State Widget build(BuildContext context) { return Column( children: [ - SizedBox( - height: topPadding, - ), - FlowyRichText( - key: _richTextKey, - textSpanDecorator: _textSpanDecorator, - textNode: widget.textNode, - editorState: widget.editorState, - ), - SizedBox( - height: bottomPadding, - ), + Padding( + padding: EdgeInsets.only( + top: topPadding, + bottom: bottomPadding, + ), + child: FlowyRichText( + key: _richTextKey, + textSpanDecorator: _textSpanDecorator, + textNode: widget.textNode, + editorState: widget.editorState, + ), + ) ], ); } From ed1dc8ccef128f6c15624272d634e5239d5ddff7 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 14:29:37 +0800 Subject: [PATCH 090/121] feat: refactor render plugin service 1. abstract render plugin as service. 2. simplify plugin development. 3. delete unused code --- .../flowy_editor/example/assets/example.json | 11 + .../flowy_editor/example/lib/main.dart | 20 +- .../lib/plugin/debuggable_rich_text.dart | 102 --- .../lib/plugin/document_node_widget.dart | 52 -- .../lib/plugin/flowy_selectable_text.dart | 758 ------------------ .../example/lib/plugin/image_node_widget.dart | 33 +- .../lib/plugin/old_text_node_widget.dart | 352 -------- .../lib/plugin/selected_text_node_widget.dart | 281 ------- .../text_with_check_box_node_widget.dart | 33 - .../plugin/text_with_heading_node_widget.dart | 46 -- .../flowy_editor/lib/editor_state.dart | 29 - .../flowy_editor/lib/flowy_editor.dart | 3 +- .../lib/render/editor/editor_entry.dart | 58 ++ .../lib/render/node_widget_builder.dart | 63 -- .../lib/render/render_plugins.dart | 88 -- .../render/rich_text/bulleted_list_text.dart | 23 +- .../lib/render/rich_text/checkbox_text.dart | 34 +- .../lib/render/rich_text/flowy_rich_text.dart | 31 +- .../lib/render/rich_text/heading_text.dart | 23 +- .../render/rich_text/number_list_text.dart | 23 +- .../lib/render/rich_text/quoted_text.dart | 24 +- .../lib/render/rich_text/rich_text_style.dart | 13 +- .../lib/service/editor_service.dart | 51 +- .../lib/service/render_plugin_service.dart | 131 +++ .../flowy_editor/lib/service/service.dart | 4 + .../flowy_editor/test/operation_test.dart | 4 +- 26 files changed, 352 insertions(+), 1938 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 6a0fba3021..2e982f98e4 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -3,6 +3,17 @@ "type": "editor", "attributes": {}, "children": [ + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "subtype": "quote" + } + }, { "type": "image", "attributes": { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 6105703fa0..7ebb340f2d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -1,12 +1,7 @@ import 'dart:convert'; import 'package:example/expandable_floating_action_button.dart'; -import 'package:example/plugin/document_node_widget.dart'; -import 'package:example/plugin/selected_text_node_widget.dart'; -import 'package:example/plugin/text_with_heading_node_widget.dart'; import 'package:example/plugin/image_node_widget.dart'; -import 'package:example/plugin/old_text_node_widget.dart'; -import 'package:example/plugin/text_with_check_box_node_widget.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/services.dart'; @@ -59,19 +54,8 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - final RenderPlugins renderPlugins = RenderPlugins(); late EditorState _editorState; int page = 0; - @override - void initState() { - super.initState(); - - renderPlugins - ..register('editor', EditorNodeWidgetBuilder.create) - ..register('image', ImageNodeBuilder.create) - ..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create) - ..register('text/with-heading', TextWithHeadingNodeBuilder.create); - } @override Widget build(BuildContext context) { @@ -130,11 +114,13 @@ class _MyHomePageState extends State { final document = StateTree.fromJson(data); _editorState = EditorState( document: document, - renderPlugins: renderPlugins, ); return FlowyEditor( editorState: _editorState, keyEventHandlers: const [], + customBuilders: { + 'image': ImageNodeBuilder(), + }, shortcuts: [ // TODO: this won't work, just a example for now. { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart deleted file mode 100644 index 6028774ba9..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -class DebuggableRichText extends StatefulWidget { - final InlineSpan text; - final GlobalKey textKey; - - const DebuggableRichText({ - Key? key, - required this.text, - required this.textKey, - }) : super(key: key); - - @override - State createState() => _DebuggableRichTextState(); -} - -class _DebuggableRichTextState extends State { - final List _textRects = []; - - RenderParagraph get _renderParagraph => - widget.textKey.currentContext?.findRenderObject() as RenderParagraph; - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _updateTextRects(); - }); - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - CustomPaint( - painter: _BoxPainter( - rects: _textRects, - ), - ), - RichText( - key: widget.textKey, - text: widget.text, - ), - ], - ); - } - - void _updateTextRects() { - setState(() { - _textRects - ..clear() - ..addAll( - _computeLocalSelectionRects( - TextSelection( - baseOffset: 0, - extentOffset: widget.text.toPlainText().length, - ), - ), - ); - }); - } - - List _computeLocalSelectionRects(TextSelection selection) { - final textBoxes = _renderParagraph.getBoxesForSelection(selection); - return textBoxes.map((box) => box.toRect()).toList(); - } -} - -class _BoxPainter extends CustomPainter { - final List _rects; - final Paint _paint; - - _BoxPainter({ - required List rects, - bool fill = false, - }) : _rects = rects, - _paint = Paint() { - _paint.style = fill ? PaintingStyle.fill : PaintingStyle.stroke; - } - - @override - void paint(Canvas canvas, Size size) { - for (final rect in _rects) { - canvas.drawRect( - rect, - _paint - ..color = Color( - (Random().nextDouble() * 0xFFFFFF).toInt(), - ).withOpacity(1.0), - ); - } - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return true; - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart deleted file mode 100644 index 2a70da2ba2..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/material.dart'; - -class EditorNodeWidgetBuilder extends NodeWidgetBuilder { - EditorNodeWidgetBuilder.create({ - required super.editorState, - required super.node, - required super.key, - }) : super.create(); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - key: key, - child: _EditorNodeWidget( - node: node, - editorState: editorState, - ), - ); - } -} - -class _EditorNodeWidget extends StatelessWidget { - final Node node; - final EditorState editorState; - - const _EditorNodeWidget({ - Key? key, - required this.node, - required this.editorState, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: e, - editorState: editorState, - ), - ), - ) - .toList(), - ), - ); - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart deleted file mode 100644 index 0454f1cdc1..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart +++ /dev/null @@ -1,758 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; - -import 'package:flutter/material.dart'; - -/// An eyeballed value that moves the cursor slightly left of where it is -/// rendered for text on Android so its positioning more accurately matches the -/// native iOS text cursor positioning. -/// -/// This value is in device pixels, not logical pixels as is typically used -/// throughout the codebase. -const int iOSHorizontalOffset = -2; - -class _TextSpanEditingController extends TextEditingController { - _TextSpanEditingController({required TextSpan textSpan}) - : assert(textSpan != null), - _textSpan = textSpan, - super(text: textSpan.toPlainText(includeSemanticsLabels: false)); - - final TextSpan _textSpan; - - @override - TextSpan buildTextSpan( - {required BuildContext context, - TextStyle? style, - required bool withComposing}) { - // This does not care about composing. - return TextSpan( - style: style, - children: [_textSpan], - ); - } - - @override - set text(String? newText) { - // This should never be reached. - throw UnimplementedError(); - } -} - -class _SelectableTextSelectionGestureDetectorBuilder - extends TextSelectionGestureDetectorBuilder { - _SelectableTextSelectionGestureDetectorBuilder({ - required _FlowySelectableTextState state, - }) : _state = state, - super(delegate: state); - - final _FlowySelectableTextState _state; - - @override - void onForcePressStart(ForcePressDetails details) { - super.onForcePressStart(details); - if (delegate.selectionEnabled && shouldShowSelectionToolbar) { - editableText.showToolbar(); - } - } - - @override - void onForcePressEnd(ForcePressDetails details) { - // Not required. - } - - @override - void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (delegate.selectionEnabled) { - renderEditable.selectWordsInRange( - from: details.globalPosition - details.offsetFromOrigin, - to: details.globalPosition, - cause: SelectionChangedCause.longPress, - ); - } - } - - @override - void onSingleTapUp(TapUpDetails details) { - editableText.hideToolbar(); - if (delegate.selectionEnabled) { - switch (Theme.of(_state.context).platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - // renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); - // break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - renderEditable.selectPosition(cause: SelectionChangedCause.tap); - break; - } - } - _state.widget.onTap?.call(); - } - - @override - void onSingleLongTapStart(LongPressStartDetails details) { - if (delegate.selectionEnabled) { - renderEditable.selectWord(cause: SelectionChangedCause.longPress); - Feedback.forLongPress(_state.context); - } - } -} - -/// A run of selectable text with a single style. -/// -/// The [FlowySelectableText] widget displays a string of text with a single style. -/// The string might break across multiple lines or might all be displayed on -/// the same line depending on the layout constraints. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc} -/// -/// The [style] argument is optional. When omitted, the text will use the style -/// from the closest enclosing [DefaultTextStyle]. If the given style's -/// [TextStyle.inherit] property is true (the default), the given style will -/// be merged with the closest enclosing [DefaultTextStyle]. This merging -/// behavior is useful, for example, to make the text bold while using the -/// default font family and size. -/// -/// {@macro flutter.material.textfield.wantKeepAlive} -/// -/// {@tool snippet} -/// -/// ```dart -/// const SelectableText( -/// 'Hello! How are you?', -/// textAlign: TextAlign.center, -/// style: TextStyle(fontWeight: FontWeight.bold), -/// ) -/// ``` -/// {@end-tool} -/// -/// Using the [SelectableText.rich] constructor, the [FlowySelectableText] widget can -/// display a paragraph with differently styled [TextSpan]s. The sample -/// that follows displays "Hello beautiful world" with different styles -/// for each word. -/// -/// {@tool snippet} -/// -/// ```dart -/// const SelectableText.rich( -/// TextSpan( -/// text: 'Hello', // default text style -/// children: [ -/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)), -/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)), -/// ], -/// ), -/// ) -/// ``` -/// {@end-tool} -/// -/// ## Interactivity -/// -/// To make [FlowySelectableText] react to touch events, use callback [onTap] to achieve -/// the desired behavior. -/// -/// See also: -/// -/// * [Text], which is the non selectable version of this widget. -/// * [TextField], which is the editable version of this widget. -class FlowySelectableText extends StatefulWidget { - /// Creates a selectable text widget. - /// - /// If the [style] argument is null, the text will use the style from the - /// closest enclosing [DefaultTextStyle]. - /// - - /// The [showCursor], [autofocus], [dragStartBehavior], [selectionHeightStyle], - /// [selectionWidthStyle] and [data] parameters must not be null. If specified, - /// the [maxLines] argument must be greater than zero. - const FlowySelectableText( - String this.data, { - Key? key, - this.focusNode, - this.style, - this.strutStyle, - this.textAlign, - this.textDirection, - this.textScaleFactor, - this.showCursor = false, - this.autofocus = false, - ToolbarOptions? toolbarOptions, - this.minLines, - this.maxLines, - this.cursorWidth = 2.0, - this.cursorHeight, - this.cursorRadius, - this.cursorColor, - this.selectionHeightStyle = ui.BoxHeightStyle.tight, - this.selectionWidthStyle = ui.BoxWidthStyle.tight, - this.dragStartBehavior = DragStartBehavior.start, - this.enableInteractiveSelection = true, - this.selectionControls, - this.onTap, - this.scrollPhysics, - this.semanticsLabel, - this.textHeightBehavior, - this.textWidthBasis, - this.onSelectionChanged, - }) : assert(showCursor != null), - assert(autofocus != null), - assert(dragStartBehavior != null), - assert(selectionHeightStyle != null), - assert(selectionWidthStyle != null), - assert(maxLines == null || maxLines > 0), - assert(minLines == null || minLines > 0), - assert( - (maxLines == null) || (minLines == null) || (maxLines >= minLines), - "minLines can't be greater than maxLines", - ), - assert( - data != null, - 'A non-null String must be provided to a SelectableText widget.', - ), - textSpan = null, - toolbarOptions = toolbarOptions ?? - const ToolbarOptions( - selectAll: true, - copy: true, - ), - super(key: key); - - /// Creates a selectable text widget with a [TextSpan]. - /// - /// The [textSpan] parameter must not be null and only contain [TextSpan] in - /// [textSpan].children. Other type of [InlineSpan] is not allowed. - /// - /// The [autofocus] and [dragStartBehavior] arguments must not be null. - const FlowySelectableText.rich( - TextSpan this.textSpan, { - Key? key, - this.focusNode, - this.style, - this.strutStyle, - this.textAlign, - this.textDirection, - this.textScaleFactor, - this.showCursor = false, - this.autofocus = false, - ToolbarOptions? toolbarOptions, - this.minLines, - this.maxLines, - this.cursorWidth = 2.0, - this.cursorHeight, - this.cursorRadius, - this.cursorColor, - this.selectionHeightStyle = ui.BoxHeightStyle.tight, - this.selectionWidthStyle = ui.BoxWidthStyle.tight, - this.dragStartBehavior = DragStartBehavior.start, - this.enableInteractiveSelection = true, - this.selectionControls, - this.onTap, - this.scrollPhysics, - this.semanticsLabel, - this.textHeightBehavior, - this.textWidthBasis, - this.onSelectionChanged, - }) : assert(showCursor != null), - assert(autofocus != null), - assert(dragStartBehavior != null), - assert(maxLines == null || maxLines > 0), - assert(minLines == null || minLines > 0), - assert( - (maxLines == null) || (minLines == null) || (maxLines >= minLines), - "minLines can't be greater than maxLines", - ), - assert( - textSpan != null, - 'A non-null TextSpan must be provided to a SelectableText.rich widget.', - ), - data = null, - toolbarOptions = toolbarOptions ?? - const ToolbarOptions( - selectAll: true, - copy: true, - ), - super(key: key); - - /// The text to display. - /// - /// This will be null if a [textSpan] is provided instead. - final String? data; - - /// The text to display as a [TextSpan]. - /// - /// This will be null if [data] is provided instead. - final TextSpan? textSpan; - - /// Defines the focus for this widget. - /// - /// Text is only selectable when widget is focused. - /// - /// The [focusNode] is a long-lived object that's typically managed by a - /// [StatefulWidget] parent. See [FocusNode] for more information. - /// - /// To give the focus to this widget, provide a [focusNode] and then - /// use the current [FocusScope] to request the focus: - /// - /// ```dart - /// FocusScope.of(context).requestFocus(myFocusNode); - /// ``` - /// - /// This happens automatically when the widget is tapped. - /// - /// To be notified when the widget gains or loses the focus, add a listener - /// to the [focusNode]: - /// - /// ```dart - /// focusNode.addListener(() { print(myFocusNode.hasFocus); }); - /// ``` - /// - /// If null, this widget will create its own [FocusNode] with - /// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget - /// to be skipped over during focus traversal. - final FocusNode? focusNode; - - /// The style to use for the text. - /// - /// If null, defaults [DefaultTextStyle] of context. - final TextStyle? style; - - /// {@macro flutter.widgets.editableText.strutStyle} - final StrutStyle? strutStyle; - - /// {@macro flutter.widgets.editableText.textAlign} - final TextAlign? textAlign; - - /// {@macro flutter.widgets.editableText.textDirection} - final TextDirection? textDirection; - - /// {@macro flutter.widgets.editableText.textScaleFactor} - final double? textScaleFactor; - - /// {@macro flutter.widgets.editableText.autofocus} - final bool autofocus; - - /// {@macro flutter.widgets.editableText.minLines} - final int? minLines; - - /// {@macro flutter.widgets.editableText.maxLines} - final int? maxLines; - - /// {@macro flutter.widgets.editableText.showCursor} - final bool showCursor; - - /// {@macro flutter.widgets.editableText.cursorWidth} - final double cursorWidth; - - /// {@macro flutter.widgets.editableText.cursorHeight} - final double? cursorHeight; - - /// {@macro flutter.widgets.editableText.cursorRadius} - final Radius? cursorRadius; - - /// The color to use when painting the cursor. - /// - /// Defaults to the theme's `cursorColor` when null. - final Color? cursorColor; - - /// Controls how tall the selection highlight boxes are computed to be. - /// - /// See [ui.BoxHeightStyle] for details on available styles. - final ui.BoxHeightStyle selectionHeightStyle; - - /// Controls how wide the selection highlight boxes are computed to be. - /// - /// See [ui.BoxWidthStyle] for details on available styles. - final ui.BoxWidthStyle selectionWidthStyle; - - /// {@macro flutter.widgets.editableText.enableInteractiveSelection} - final bool enableInteractiveSelection; - - /// {@macro flutter.widgets.editableText.selectionControls} - final TextSelectionControls? selectionControls; - - /// {@macro flutter.widgets.scrollable.dragStartBehavior} - final DragStartBehavior dragStartBehavior; - - /// Configuration of toolbar options. - /// - /// Paste and cut will be disabled regardless. - /// - /// If not set, select all and copy will be enabled by default. - final ToolbarOptions toolbarOptions; - - /// {@macro flutter.widgets.editableText.selectionEnabled} - bool get selectionEnabled => enableInteractiveSelection; - - /// Called when the user taps on this selectable text. - /// - /// The selectable text builds a [GestureDetector] to handle input events like tap, - /// to trigger focus requests, to move the caret, adjust the selection, etc. - /// Handling some of those events by wrapping the selectable text with a competing - /// GestureDetector is problematic. - /// - /// To unconditionally handle taps, without interfering with the selectable text's - /// internal gesture detector, provide this callback. - /// - /// To be notified when the text field gains or loses the focus, provide a - /// [focusNode] and add a listener to that. - /// - /// To listen to arbitrary pointer events without competing with the - /// selectable text's internal gesture detector, use a [Listener]. - final GestureTapCallback? onTap; - - /// {@macro flutter.widgets.editableText.scrollPhysics} - final ScrollPhysics? scrollPhysics; - - /// {@macro flutter.widgets.Text.semanticsLabel} - final String? semanticsLabel; - - /// {@macro dart.ui.textHeightBehavior} - final TextHeightBehavior? textHeightBehavior; - - /// {@macro flutter.painting.textPainter.textWidthBasis} - final TextWidthBasis? textWidthBasis; - - /// {@macro flutter.widgets.editableText.onSelectionChanged} - final SelectionChangedCallback? onSelectionChanged; - - @override - State createState() => _FlowySelectableTextState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - .add(DiagnosticsProperty('data', data, defaultValue: null)); - properties.add(DiagnosticsProperty('semanticsLabel', semanticsLabel, - defaultValue: null)); - properties.add(DiagnosticsProperty('focusNode', focusNode, - defaultValue: null)); - properties.add( - DiagnosticsProperty('style', style, defaultValue: null)); - properties.add( - DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); - properties.add(DiagnosticsProperty('showCursor', showCursor, - defaultValue: false)); - properties.add(IntProperty('minLines', minLines, defaultValue: null)); - properties.add(IntProperty('maxLines', maxLines, defaultValue: null)); - properties.add( - EnumProperty('textAlign', textAlign, defaultValue: null)); - properties.add(EnumProperty('textDirection', textDirection, - defaultValue: null)); - properties.add( - DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null)); - properties - .add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); - properties - .add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); - properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, - defaultValue: null)); - properties.add(DiagnosticsProperty('cursorColor', cursorColor, - defaultValue: null)); - properties.add(FlagProperty('selectionEnabled', - value: selectionEnabled, - defaultValue: true, - ifFalse: 'selection disabled')); - properties.add(DiagnosticsProperty( - 'selectionControls', selectionControls, - defaultValue: null)); - properties.add(DiagnosticsProperty( - 'scrollPhysics', scrollPhysics, - defaultValue: null)); - properties.add(DiagnosticsProperty( - 'textHeightBehavior', textHeightBehavior, - defaultValue: null)); - } -} - -class _FlowySelectableTextState extends State - implements TextSelectionGestureDetectorBuilderDelegate { - EditableTextState? get _editableText => editableTextKey.currentState; - - late _TextSpanEditingController _controller; - - FocusNode? _focusNode; - FocusNode get _effectiveFocusNode => - widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true)); - - bool _showSelectionHandles = false; - - late _SelectableTextSelectionGestureDetectorBuilder - _selectionGestureDetectorBuilder; - - // API for TextSelectionGestureDetectorBuilderDelegate. - @override - late bool forcePressEnabled; - - @override - final GlobalKey editableTextKey = - GlobalKey(); - - @override - bool get selectionEnabled => widget.selectionEnabled; - // End of API for TextSelectionGestureDetectorBuilderDelegate. - - @override - void initState() { - super.initState(); - _selectionGestureDetectorBuilder = - _SelectableTextSelectionGestureDetectorBuilder(state: this); - _controller = _TextSpanEditingController( - textSpan: widget.textSpan ?? TextSpan(text: widget.data), - ); - _controller.addListener(_onControllerChanged); - } - - @override - void didUpdateWidget(FlowySelectableText oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.data != oldWidget.data || - widget.textSpan != oldWidget.textSpan) { - _controller.removeListener(_onControllerChanged); - _controller = _TextSpanEditingController( - textSpan: widget.textSpan ?? TextSpan(text: widget.data), - ); - _controller.addListener(_onControllerChanged); - } - if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) { - _showSelectionHandles = false; - } else { - _showSelectionHandles = true; - } - } - - @override - void dispose() { - _focusNode?.dispose(); - _controller.removeListener(_onControllerChanged); - super.dispose(); - } - - void _onControllerChanged() { - final bool showSelectionHandles = - !_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed; - if (showSelectionHandles == _showSelectionHandles) { - return; - } - setState(() { - _showSelectionHandles = showSelectionHandles; - }); - } - - TextSelection? _lastSeenTextSelection; - - void _handleSelectionChanged( - TextSelection selection, SelectionChangedCause? cause) { - final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); - if (willShowSelectionHandles != _showSelectionHandles) { - setState(() { - _showSelectionHandles = willShowSelectionHandles; - }); - } - // TODO(chunhtai): The selection may be the same. We should remove this - // check once this is fixed https://github.com/flutter/flutter/issues/76349. - if (widget.onSelectionChanged != null && - _lastSeenTextSelection != selection) { - widget.onSelectionChanged!(selection, cause); - } - _lastSeenTextSelection = selection; - - switch (Theme.of(context).platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - if (cause == SelectionChangedCause.longPress) { - _editableText?.bringIntoView(selection.base); - } - return; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - // Do nothing. - } - } - - /// Toggle the toolbar when a selection handle is tapped. - void _handleSelectionHandleTapped() { - if (_controller.selection.isCollapsed) { - _editableText!.toggleToolbar(); - } - } - - bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { - // When the text field is activated by something that doesn't trigger the - // selection overlay, we shouldn't show the handles either. - if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) - return false; - - if (_controller.selection.isCollapsed) return false; - - if (cause == SelectionChangedCause.keyboard) return false; - - if (cause == SelectionChangedCause.longPress) return true; - - if (_controller.text.isNotEmpty) return true; - - return false; - } - - @override - Widget build(BuildContext context) { - // TODO(garyq): Assert to block WidgetSpans from being used here are removed, - // but we still do not yet have nice handling of things like carets, clipboard, - // and other features. We should add proper support. Currently, caret handling - // is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010 - // should be landed in SkParagraph after the switch is complete. - assert(debugCheckHasMediaQuery(context)); - assert(debugCheckHasDirectionality(context)); - assert( - !(widget.style != null && - widget.style!.inherit == false && - (widget.style!.fontSize == null || - widget.style!.textBaseline == null)), - 'inherit false style must supply fontSize and textBaseline', - ); - - final ThemeData theme = Theme.of(context); - final TextSelectionThemeData selectionTheme = - TextSelectionTheme.of(context); - final FocusNode focusNode = _effectiveFocusNode; - - TextSelectionControls? textSelectionControls = widget.selectionControls; - final bool paintCursorAboveText; - final bool cursorOpacityAnimates; - Offset? cursorOffset; - Color? cursorColor = widget.cursorColor; - final Color selectionColor; - Radius? cursorRadius = widget.cursorRadius; - - switch (theme.platform) { - case TargetPlatform.iOS: - final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); - forcePressEnabled = true; - textSelectionControls ??= cupertinoTextSelectionControls; - paintCursorAboveText = true; - cursorOpacityAnimates = true; - cursorColor ??= - selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; - selectionColor = selectionTheme.selectionColor ?? - cupertinoTheme.primaryColor.withOpacity(0.40); - cursorRadius ??= const Radius.circular(2.0); - cursorOffset = Offset( - iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); - break; - - case TargetPlatform.macOS: - final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); - forcePressEnabled = false; - textSelectionControls ??= cupertinoDesktopTextSelectionControls; - paintCursorAboveText = true; - cursorOpacityAnimates = true; - cursorColor ??= - selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; - selectionColor = selectionTheme.selectionColor ?? - cupertinoTheme.primaryColor.withOpacity(0.40); - cursorRadius ??= const Radius.circular(2.0); - cursorOffset = Offset( - iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); - break; - - case TargetPlatform.android: - case TargetPlatform.fuchsia: - forcePressEnabled = false; - textSelectionControls ??= materialTextSelectionControls; - paintCursorAboveText = false; - cursorOpacityAnimates = false; - cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; - selectionColor = selectionTheme.selectionColor ?? - theme.colorScheme.primary.withOpacity(0.40); - break; - - case TargetPlatform.linux: - case TargetPlatform.windows: - forcePressEnabled = false; - textSelectionControls ??= desktopTextSelectionControls; - paintCursorAboveText = false; - cursorOpacityAnimates = false; - cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; - selectionColor = selectionTheme.selectionColor ?? - theme.colorScheme.primary.withOpacity(0.40); - break; - } - - final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); - TextStyle? effectiveTextStyle = widget.style; - if (effectiveTextStyle == null || effectiveTextStyle.inherit) - effectiveTextStyle = defaultTextStyle.style.merge(widget.style); - if (MediaQuery.boldTextOverride(context)) - effectiveTextStyle = effectiveTextStyle - .merge(const TextStyle(fontWeight: FontWeight.bold)); - final Widget child = RepaintBoundary( - child: EditableText( - key: editableTextKey, - style: effectiveTextStyle, - readOnly: true, - textWidthBasis: - widget.textWidthBasis ?? defaultTextStyle.textWidthBasis, - textHeightBehavior: - widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior, - showSelectionHandles: _showSelectionHandles, - showCursor: widget.showCursor, - controller: _controller, - focusNode: focusNode, - strutStyle: widget.strutStyle ?? const StrutStyle(), - textAlign: - widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, - textDirection: widget.textDirection, - textScaleFactor: widget.textScaleFactor, - autofocus: widget.autofocus, - forceLine: false, - toolbarOptions: widget.toolbarOptions, - minLines: widget.minLines, - maxLines: widget.maxLines ?? defaultTextStyle.maxLines, - selectionColor: selectionColor, - selectionControls: - widget.selectionEnabled ? textSelectionControls : null, - onSelectionChanged: _handleSelectionChanged, - onSelectionHandleTapped: _handleSelectionHandleTapped, - rendererIgnoresPointer: true, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: cursorRadius, - cursorColor: cursorColor, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - cursorOpacityAnimates: cursorOpacityAnimates, - cursorOffset: cursorOffset, - paintCursorAboveText: paintCursorAboveText, - backgroundCursorColor: CupertinoColors.inactiveGray, - enableInteractiveSelection: widget.enableInteractiveSelection, - dragStartBehavior: widget.dragStartBehavior, - scrollPhysics: widget.scrollPhysics, - autofillHints: null, - ), - ); - - return Semantics( - label: widget.semanticsLabel, - excludeSemantics: widget.semanticsLabel != null, - onLongPress: () { - _effectiveFocusNode.requestFocus(); - }, - child: _selectionGestureDetectorBuilder.buildGestureDetector( - behavior: HitTestBehavior.translucent, - child: child, - ), - ); - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index b4a026a9f0..25d432b759 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,40 +1,37 @@ -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; -class ImageNodeBuilder extends NodeWidgetBuilder { - ImageNodeBuilder.create({ - required super.node, - required super.editorState, - required super.key, - }) : super.create(); - +class ImageNodeBuilder extends NodeWidgetBuilder { @override - Widget build(BuildContext context) { - return _ImageNodeWidget( - key: key, - node: node, - editorState: editorState, + Widget build(NodeWidgetContext context) { + return ImageNodeWidget( + key: context.node.key, + node: context.node, + editorState: context.editorState, ); } + + @override + NodeValidator get nodeValidator => ((node) { + return node.type == 'image'; + }); } -class _ImageNodeWidget extends StatefulWidget { +class ImageNodeWidget extends StatefulWidget { final Node node; final EditorState editorState; - const _ImageNodeWidget({ + const ImageNodeWidget({ Key? key, required this.node, required this.editorState, }) : super(key: key); @override - State<_ImageNodeWidget> createState() => __ImageNodeWidgetState(); + State createState() => _ImageNodeWidgetState(); } -class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { +class _ImageNodeWidgetState extends State with Selectable { Node get node => widget.node; EditorState get editorState => widget.editorState; String get src => widget.node.attributes['image_src'] as String; diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart deleted file mode 100644 index bad07fe6a6..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart +++ /dev/null @@ -1,352 +0,0 @@ -// import 'package:flowy_editor/document/position.dart'; -// import 'package:flowy_editor/document/selection.dart'; -// import 'package:flutter/gestures.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flowy_editor/flowy_editor.dart'; -// import 'package:flutter/services.dart'; -// import 'package:url_launcher/url_launcher_string.dart'; -// import 'flowy_selectable_text.dart'; - -// class TextNodeBuilder extends NodeWidgetBuilder { -// TextNodeBuilder.create({ -// required super.node, -// required super.editorState, -// required super.key, -// }) : super.create() { -// nodeValidator = ((node) { -// return node.type == 'text'; -// }); -// } - -// @override -// Widget build(BuildContext context) { -// return _TextNodeWidget(key: key, node: node, editorState: editorState); -// } -// } - -// class _TextNodeWidget extends StatefulWidget { -// final Node node; -// final EditorState editorState; - -// const _TextNodeWidget({ -// Key? key, -// required this.node, -// required this.editorState, -// }) : super(key: key); - -// @override -// State<_TextNodeWidget> createState() => __TextNodeWidgetState(); -// } - -// class __TextNodeWidgetState extends State<_TextNodeWidget> -// implements DeltaTextInputClient { -// TextNode get node => widget.node as TextNode; -// EditorState get editorState => widget.editorState; -// bool _metaKeyDown = false; -// bool _shiftKeyDown = false; - -// TextInputConnection? _textInputConnection; - -// @override -// Widget build(BuildContext context) { -// return Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// FlowySelectableText.rich( -// node.toTextSpan(), -// showCursor: true, -// enableInteractiveSelection: true, -// onSelectionChanged: _onSelectionChanged, -// // autofocus: true, -// focusNode: FocusNode( -// onKey: _onKey, -// ), -// ), -// if (node.children.isNotEmpty) -// ...node.children.map( -// (e) => editorState.renderPlugins.buildWidget( -// context: NodeWidgetContext( -// buildContext: context, -// node: e, -// editorState: editorState, -// ), -// ), -// ), -// const SizedBox( -// height: 10, -// ), -// ], -// ); -// } - -// KeyEventResult _onKey(FocusNode focusNode, RawKeyEvent event) { -// debugPrint('key: $event'); -// if (event is RawKeyDownEvent) { -// final sel = _globalSelectionToLocal(node, editorState.cursorSelection); -// if (event.logicalKey == LogicalKeyboardKey.backspace) { -// _backDeleteTextAtSelection(sel); -// return KeyEventResult.handled; -// } else if (event.logicalKey == LogicalKeyboardKey.delete) { -// _forwardDeleteTextAtSelection(sel); -// return KeyEventResult.handled; -// } else if (event.logicalKey == LogicalKeyboardKey.metaLeft || -// event.logicalKey == LogicalKeyboardKey.metaRight) { -// _metaKeyDown = true; -// } else if (event.logicalKey == LogicalKeyboardKey.shiftLeft || -// event.logicalKey == LogicalKeyboardKey.shiftRight) { -// _shiftKeyDown = true; -// } else if (event.logicalKey == LogicalKeyboardKey.keyZ && _metaKeyDown) { -// if (_shiftKeyDown) { -// editorState.undoManager.redo(); -// } else { -// editorState.undoManager.undo(); -// } -// } -// } else if (event is RawKeyUpEvent) { -// if (event.logicalKey == LogicalKeyboardKey.metaLeft || -// event.logicalKey == LogicalKeyboardKey.metaRight) { -// _metaKeyDown = false; -// } -// if (event.logicalKey == LogicalKeyboardKey.shiftLeft || -// event.logicalKey == LogicalKeyboardKey.shiftRight) { -// _shiftKeyDown = false; -// } -// } -// return KeyEventResult.ignored; -// } - -// void _onSelectionChanged( -// TextSelection selection, SelectionChangedCause? cause) { -// _textInputConnection?.close(); -// _textInputConnection = TextInput.attach( -// this, -// const TextInputConfiguration( -// enableDeltaModel: true, -// inputType: TextInputType.multiline, -// textCapitalization: TextCapitalization.sentences, -// ), -// ); -// editorState.cursorSelection = _localSelectionToGlobal(node, selection); -// _textInputConnection -// ?..show() -// ..setEditingState( -// TextEditingValue( -// text: node.toRawString(), -// selection: selection, -// ), -// ); -// } - -// _backDeleteTextAtSelection(TextSelection? sel) { -// if (sel == null) { -// return; -// } -// if (sel.start == 0) { -// return; -// } - -// if (sel.isCollapsed) { -// TransactionBuilder(editorState) -// ..deleteText(node, sel.start - 1, 1) -// ..commit(); -// } else { -// TransactionBuilder(editorState) -// ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset) -// ..commit(); -// } - -// _setEditingStateFromGlobal(); -// } - -// _forwardDeleteTextAtSelection(TextSelection? sel) { -// if (sel == null) { -// return; -// } - -// if (sel.isCollapsed) { -// TransactionBuilder(editorState) -// ..deleteText(node, sel.start, 1) -// ..commit(); -// } else { -// TransactionBuilder(editorState) -// ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset) -// ..commit(); -// } -// _setEditingStateFromGlobal(); -// } - -// _setEditingStateFromGlobal() { -// _textInputConnection?.setEditingState(TextEditingValue( -// text: node.toRawString(), -// selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? -// const TextSelection.collapsed(offset: 0))); -// } - -// @override -// void connectionClosed() { -// // TODO: implement connectionClosed -// } - -// @override -// // TODO: implement currentAutofillScope -// AutofillScope? get currentAutofillScope => throw UnimplementedError(); - -// @override -// // TODO: implement currentTextEditingValue -// TextEditingValue? get currentTextEditingValue => TextEditingValue( -// text: node.toRawString(), -// selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? -// const TextSelection.collapsed(offset: 0)); - -// @override -// void insertTextPlaceholder(Size size) { -// // TODO: implement insertTextPlaceholder -// } - -// @override -// void performAction(TextInputAction action) {} - -// @override -// void performPrivateCommand(String action, Map data) { -// // TODO: implement performPrivateCommand -// } - -// @override -// void removeTextPlaceholder() { -// // TODO: implement removeTextPlaceholder -// } - -// @override -// void showAutocorrectionPromptRect(int start, int end) { -// // TODO: implement showAutocorrectionPromptRect -// } - -// @override -// void showToolbar() { -// // TODO: implement showToolbar -// } - -// @override -// void updateEditingValue(TextEditingValue value) {} - -// @override -// void updateEditingValueWithDeltas(List textEditingDeltas) { -// for (final textDelta in textEditingDeltas) { -// if (textDelta is TextEditingDeltaInsertion) { -// TransactionBuilder(editorState) -// ..insertText(node, textDelta.insertionOffset, textDelta.textInserted) -// ..commit(); -// } else if (textDelta is TextEditingDeltaDeletion) { -// TransactionBuilder(editorState) -// ..deleteText(node, textDelta.deletedRange.start, -// textDelta.deletedRange.end - textDelta.deletedRange.start) -// ..commit(); -// } -// } -// } - -// @override -// void updateFloatingCursor(RawFloatingCursorPoint point) { -// // TODO: implement updateFloatingCursor -// } -// } - -// extension on TextNode { -// TextSpan toTextSpan() => TextSpan( -// children: delta.operations -// .whereType() -// .map((op) => op.toTextSpan()) -// .toList()); -// } - -// extension on TextInsert { -// TextSpan toTextSpan() { -// FontWeight? fontWeight; -// FontStyle? fontStyle; -// TextDecoration? decoration; -// GestureRecognizer? gestureRecognizer; -// Color? color; -// Color highLightColor = Colors.transparent; -// double fontSize = 16.0; -// final attributes = this.attributes; -// if (attributes?['bold'] == true) { -// fontWeight = FontWeight.bold; -// } -// if (attributes?['italic'] == true) { -// fontStyle = FontStyle.italic; -// } -// if (attributes?['underline'] == true) { -// decoration = TextDecoration.underline; -// } -// if (attributes?['strikethrough'] == true) { -// decoration = TextDecoration.lineThrough; -// } -// if (attributes?['highlight'] is String) { -// highLightColor = Color(int.parse(attributes!['highlight'])); -// } -// if (attributes?['href'] is String) { -// color = const Color.fromARGB(255, 55, 120, 245); -// decoration = TextDecoration.underline; -// gestureRecognizer = TapGestureRecognizer() -// ..onTap = () { -// launchUrlString(attributes?['href']); -// }; -// } -// final heading = attributes?['heading'] as String?; -// if (heading != null) { -// // TODO: make it better -// if (heading == 'h1') { -// fontSize = 30.0; -// } else if (heading == 'h2') { -// fontSize = 20.0; -// } -// fontWeight = FontWeight.bold; -// } -// return TextSpan( -// text: content, -// style: TextStyle( -// fontWeight: fontWeight, -// fontStyle: fontStyle, -// decoration: decoration, -// color: color, -// fontSize: fontSize, -// backgroundColor: highLightColor, -// ), -// recognizer: gestureRecognizer, -// ); -// } -// } - -// TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) { -// if (globalSel == null) { -// return null; -// } -// final nodePath = node.path; - -// if (!pathEquals(nodePath, globalSel.start.path)) { -// return null; -// } -// if (globalSel.isCollapsed) { -// return TextSelection( -// baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset); -// } else { -// if (pathEquals(globalSel.start.path, globalSel.end.path)) { -// return TextSelection( -// baseOffset: globalSel.start.offset, -// extentOffset: globalSel.end.offset); -// } -// } -// return null; -// } - -// Selection? _localSelectionToGlobal(Node node, TextSelection? sel) { -// if (sel == null) { -// return null; -// } -// final nodePath = node.path; - -// return Selection( -// start: Position(path: nodePath, offset: sel.baseOffset), -// end: Position(path: nodePath, offset: sel.extentOffset), -// ); -// } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart deleted file mode 100644 index 94c6e56a5e..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ /dev/null @@ -1,281 +0,0 @@ -import 'dart:math'; - -import 'package:example/plugin/debuggable_rich_text.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -class SelectedTextNodeBuilder extends NodeWidgetBuilder { - SelectedTextNodeBuilder.create({ - required super.node, - required super.editorState, - required super.key, - }) : super.create() { - nodeValidator = ((node) { - return node.type == 'text'; - }); - } - - @override - Widget build(BuildContext context) { - return _SelectedTextNodeWidget( - key: key, - node: node, - editorState: editorState, - ); - } -} - -class _SelectedTextNodeWidget extends StatefulWidget { - final Node node; - final EditorState editorState; - - const _SelectedTextNodeWidget({ - Key? key, - required this.node, - required this.editorState, - }) : super(key: key); - - @override - State<_SelectedTextNodeWidget> createState() => - _SelectedTextNodeWidgetState(); -} - -class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> - with Selectable { - TextNode get node => widget.node as TextNode; - EditorState get editorState => widget.editorState; - - final _textKey = GlobalKey(); - TextSelection? _textSelection; - - RenderParagraph get _renderParagraph => - _textKey.currentContext?.findRenderObject() as RenderParagraph; - - @override - Selection getSelectionInRange(Offset start, Offset end) { - final localStart = _renderParagraph.globalToLocal(start); - final localEnd = _renderParagraph.globalToLocal(end); - final baseOffset = _getTextPositionAtOffset(localStart).offset; - final extentOffset = _getTextPositionAtOffset(localEnd).offset; - return Selection.single( - path: node.path, - startOffset: baseOffset, - endOffset: extentOffset, - ); - } - - @override - Offset localToGlobal(Offset offset) { - return _renderParagraph.localToGlobal(offset); - } - - @override - List getRectsInSelection(Selection selection) { - assert(pathEquals(selection.start.path, selection.end.path)); - assert(pathEquals(selection.start.path, node.path)); - final textSelection = TextSelection( - baseOffset: selection.start.offset, - extentOffset: selection.end.offset, - ); - return _computeSelectionRects(textSelection); - } - - @override - Rect getCursorRectInPosition(Position position) { - final textSelection = TextSelection.collapsed(offset: position.offset); - _textSelection = textSelection; - return _computeCursorRect(textSelection.baseOffset); - } - - @override - Position getPositionInOffset(Offset start) { - final localStart = _renderParagraph.globalToLocal(start); - final baseOffset = _getTextPositionAtOffset(localStart).offset; - return Position(path: node.path, offset: baseOffset); - } - - @override - TextSelection? getTextSelectionInSelection(Selection selection) { - assert(selection.isCollapsed); - if (!selection.isCollapsed) { - return null; - } - return TextSelection( - baseOffset: selection.start.offset, - extentOffset: selection.end.offset, - ); - } - - @override - Position start() => Position(path: node.path, offset: 0); - - @override - Position end() => - Position(path: node.path, offset: node.toRawString().length); - - @override - Widget build(BuildContext context) { - Widget richText; - if (kDebugMode) { - richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey); - } else { - richText = RichText(key: _textKey, text: node.toTextSpan()); - } - - if (node.children.isEmpty) { - return richText; - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width, - child: richText, - ), - if (node.children.isNotEmpty) - ...node.children.map( - (e) => editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: e, - editorState: editorState, - ), - ), - ), - const SizedBox( - height: 5, - ), - ], - ); - } - - TextPosition _getTextPositionAtOffset(Offset offset) { - return _renderParagraph.getPositionForOffset(offset); - } - - List _computeSelectionRects(TextSelection textSelection) { - final textBoxes = _renderParagraph.getBoxesForSelection(textSelection); - return textBoxes.map((box) => box.toRect()).toList(); - } - - Rect _computeCursorRect(int offset) { - final position = TextPosition(offset: offset); - final cursorOffset = - _renderParagraph.getOffsetForCaret(position, Rect.zero); - final cursorHeight = _renderParagraph.getFullHeightForCaret(position); - if (cursorHeight != null) { - const cursorWidth = 2; - return Rect.fromLTWH( - cursorOffset.dx - (cursorWidth / 2), - cursorOffset.dy, - cursorWidth.toDouble(), - cursorHeight.toDouble(), - ); - } else { - return Rect.zero; - } - } -} - -extension on TextNode { - TextSpan toTextSpan() => TextSpan( - children: delta.operations - .whereType() - .map((op) => op.toTextSpan()) - .toList()); -} - -extension on TextInsert { - TextSpan toTextSpan() { - FontWeight? fontWeight; - FontStyle? fontStyle; - TextDecoration? decoration; - GestureRecognizer? gestureRecognizer; - Color color = Colors.black; - Color highLightColor = Colors.transparent; - double fontSize = 16.0; - final attributes = this.attributes; - if (attributes?['bold'] == true) { - fontWeight = FontWeight.bold; - } - if (attributes?['italic'] == true) { - fontStyle = FontStyle.italic; - } - if (attributes?['underline'] == true) { - decoration = TextDecoration.underline; - } - if (attributes?['strikethrough'] == true) { - decoration = TextDecoration.lineThrough; - } - if (attributes?['highlight'] is String) { - highLightColor = Color(int.parse(attributes!['highlight'])); - } - if (attributes?['href'] is String) { - color = const Color.fromARGB(255, 55, 120, 245); - decoration = TextDecoration.underline; - gestureRecognizer = TapGestureRecognizer() - ..onTap = () { - launchUrlString(attributes?['href']); - }; - } - final heading = attributes?['heading'] as String?; - if (heading != null) { - // TODO: make it better - if (heading == 'h1') { - fontSize = 30.0; - } else if (heading == 'h2') { - fontSize = 20.0; - } - fontWeight = FontWeight.bold; - } - return TextSpan( - text: content, - style: TextStyle( - fontWeight: fontWeight, - fontStyle: fontStyle, - decoration: decoration, - color: color, - fontSize: fontSize, - backgroundColor: highLightColor, - ), - recognizer: gestureRecognizer, - ); - } -} - -class FlowyPainter extends CustomPainter { - final List _rects; - final Paint _paint; - - FlowyPainter({ - Key? key, - required Color color, - required List rects, - bool fill = false, - }) : _rects = rects, - _paint = Paint()..color = color { - _paint.style = fill ? PaintingStyle.fill : PaintingStyle.stroke; - } - - @override - void paint(Canvas canvas, Size size) { - for (final rect in _rects) { - canvas.drawRect( - rect, - _paint, - ); - } - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return true; - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart deleted file mode 100644 index f7985ed564..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/material.dart'; - -class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder { - TextWithCheckBoxNodeBuilder.create({ - required super.node, - required super.editorState, - required super.key, - }) : super.create(); - - // TODO: check the type - bool get isCompleted => node.attributes['checkbox'] as bool; - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Checkbox(value: isCompleted, onChanged: (value) {}), - Expanded( - child: renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: node, - editorState: editorState, - ), - withSubtype: false, - ), - ) - ], - ); - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart deleted file mode 100644 index c4bd027888..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/material.dart'; - -class TextWithHeadingNodeBuilder extends NodeWidgetBuilder { - TextWithHeadingNodeBuilder.create({ - required super.editorState, - required super.node, - required super.key, - }) : super.create() { - nodeValidator = (node) => node.attributes.containsKey('heading'); - } - - String get heading => node.attributes['heading'] as String; - Widget buildPadding() { - if (heading == 'h1') { - return const Padding( - padding: EdgeInsets.only(top: 10), - ); - } else if (heading == 'h2') { - return const Padding( - padding: EdgeInsets.only(top: 5), - ); - } - return const Padding( - padding: EdgeInsets.only(top: 0), - ); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - buildPadding(), - renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: node, - editorState: editorState, - ), - withSubtype: false, - ), - buildPadding(), - ], - ); - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 02d18455df..1019ad3510 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,10 +1,4 @@ import 'dart:async'; -import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; -import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; -import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/render/rich_text/heading_text.dart'; -import 'package:flowy_editor/render/rich_text/number_list_text.dart'; -import 'package:flowy_editor/render/rich_text/quoted_text.dart'; import 'package:flowy_editor/service/service.dart'; import 'package:flutter/material.dart'; @@ -14,7 +8,6 @@ import 'package:flowy_editor/document/state_tree.dart'; import 'package:flowy_editor/operation/operation.dart'; import 'package:flowy_editor/operation/transaction.dart'; import 'package:flowy_editor/undo_manager.dart'; -import 'package:flowy_editor/render/render_plugins.dart'; class ApplyOptions { /// This flag indicates that @@ -30,7 +23,6 @@ class ApplyOptions { class EditorState { final StateTree document; - final RenderPlugins renderPlugins; List selectedNodes = []; @@ -59,31 +51,10 @@ class EditorState { EditorState({ required this.document, - required this.renderPlugins, }) { - // FIXME: abstract render plugins as a service. - renderPlugins.register('text', RichTextNodeWidgetBuilder.create); - renderPlugins.register('text/checkbox', CheckboxNodeWidgetBuilder.create); - renderPlugins.register('text/heading', HeadingTextNodeWidgetBuilder.create); - renderPlugins.register( - 'text/bullet-list', BulletedListTextNodeWidgetBuilder.create); - renderPlugins.register( - 'text/number-list', NumberListTextNodeWidgetBuilder.create); - renderPlugins.register('text/quote', QuotedTextNodeWidgetBuilder.create); undoManager.state = this; } - /// TODO: move to a better place. - Widget build(BuildContext context) { - return renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: document.root, - editorState: this, - ), - ); - } - apply(Transaction transaction, [ApplyOptions options = const ApplyOptions()]) { for (final op in transaction.operations) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 91c6b1c4b0..c3e15959a6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -4,8 +4,6 @@ export 'package:flowy_editor/document/state_tree.dart'; export 'package:flowy_editor/document/node.dart'; export 'package:flowy_editor/document/path.dart'; export 'package:flowy_editor/document/text_delta.dart'; -export 'package:flowy_editor/render/render_plugins.dart'; -export 'package:flowy_editor/render/node_widget_builder.dart'; export 'package:flowy_editor/render/selection/selectable.dart'; export 'package:flowy_editor/operation/transaction.dart'; export 'package:flowy_editor/operation/transaction_builder.dart'; @@ -14,3 +12,4 @@ export 'package:flowy_editor/editor_state.dart'; export 'package:flowy_editor/service/editor_service.dart'; export 'package:flowy_editor/document/selection.dart'; export 'package:flowy_editor/document/position.dart'; +export 'package:flowy_editor/service/render_plugin_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart new file mode 100644 index 0000000000..650732f9f9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart @@ -0,0 +1,58 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flutter/material.dart'; + +class EditorEntryWidgetBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + return EditorNodeWidget( + key: context.node.key, + node: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator get nodeValidator => ((node) { + return node.type == 'editor'; + }); +} + +class EditorNodeWidget extends StatelessWidget { + const EditorNodeWidget({ + Key? key, + required this.node, + required this.editorState, + }) : super(key: key); + + final Node node; + final EditorState editorState; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (child) => + editorState.service.renderPluginService.buildPluginWidget( + child is TextNode + ? NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ) + : NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ), + ), + ) + .toList(), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart deleted file mode 100644 index 214818f60a..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/render/render_plugins.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -typedef NodeValidator = bool Function(T node); - -class NodeWidgetBuilder { - final EditorState editorState; - final T node; - final Key key; - - bool rebuildOnNodeChanged; - NodeValidator? nodeValidator; - - RenderPlugins get renderPlugins => editorState.renderPlugins; - - NodeWidgetBuilder.create({ - required this.editorState, - required this.node, - required this.key, - this.rebuildOnNodeChanged = true, - }); - - /// Render the current [Node] - /// and the layout style of [Node.Children]. - Widget build( - BuildContext context, - ) => - throw UnimplementedError(); - - /// TODO: refactore this part. - /// return widget embedded with ChangeNotifier and widget itself. - Widget call( - BuildContext context, - ) { - /// TODO: Validate the node - /// if failed, stop call build function, - /// return Empty widget, and throw Error. - if (nodeValidator != null && nodeValidator!(node) != true) { - throw Exception( - 'Node validate failure, node = { type: ${node.type}, attributes: ${node.attributes} }'); - } - - return _build(context); - } - - Widget _build(BuildContext context) { - return CompositedTransformTarget( - link: node.layerLink, - child: ChangeNotifierProvider.value( - value: node, - builder: (context, child) => Consumer( - builder: ((context, value, child) { - debugPrint('Node is rebuilding...'); - return build(context); - }), - ), - ), - ); - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart deleted file mode 100644 index efe5865d64..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/material.dart'; -import '../document/node.dart'; -import './node_widget_builder.dart'; -import 'package:flowy_editor/editor_state.dart'; - -class NodeWidgetContext { - final BuildContext buildContext; - final Node node; - final EditorState editorState; - - NodeWidgetContext({ - required this.buildContext, - required this.node, - required this.editorState, - }); -} - -typedef NodeWidgetBuilderF = A - Function({ - required T node, - required EditorState editorState, - required GlobalKey key, -}); - -// unused -// typedef NodeBuilder = T Function(Node node); - -class RenderPlugins { - final Map _nodeWidgetBuilders = {}; - // unused - // Map nodeBuilders = {}; - - /// Register plugin to render specified [name]. - /// - /// [name] should be [Node].type - /// or [Node].type + '/' + [Node].attributes['subtype']. - /// - /// e.g. 'text', 'text/with-checkbox', or 'text/with-heading' - /// - /// [name] could be empty. - void register(String name, NodeWidgetBuilderF builder) { - _validatePluginName(name); - - _nodeWidgetBuilders[name] = builder; - } - - /// UnRegister plugin with specified [name]. - void unRegister(String name) { - _validatePluginName(name); - - _nodeWidgetBuilders.removeWhere((key, _) => key == name); - } - - Widget buildWidget({ - required NodeWidgetContext context, - bool withSubtype = true, - }) { - /// Find node widget builder - /// 1. If node's attributes contains subtype, return. - /// 2. If node's attributes do no contains substype, return. - final node = context.node; - var name = node.type; - if (withSubtype && node.subtype != null) { - name += '/${node.subtype}'; - } - final nodeWidgetBuilder = _nodeWidgetBuilder(name); - final key = GlobalKey(); - node.key = key; - return nodeWidgetBuilder( - node: context.node, - editorState: context.editorState, - key: key, - )(context.buildContext); - } - - NodeWidgetBuilderF _nodeWidgetBuilder(String name) { - assert(_nodeWidgetBuilders.containsKey(name), - 'Could not query the builder with this $name'); - return _nodeWidgetBuilders[name]!; - } - - void _validatePluginName(String name) { - final paths = name.split('/'); - if (paths.length > 2) { - throw Exception('[Name] must contains zero or one slash("/")'); - } - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart index a345f7de3f..0eae3f22f2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart @@ -1,27 +1,26 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/render/node_widget_builder.dart'; import 'package:flowy_editor/render/rich_text/default_selectable.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; -class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder { - BulletedListTextNodeWidgetBuilder.create({ - required super.editorState, - required super.node, - required super.key, - }) : super.create(); - +class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder { @override - Widget build(BuildContext context) { + Widget build(NodeWidgetContext context) { return BulletedListTextNodeWidget( - key: key, - textNode: node as TextNode, - editorState: editorState, + key: context.node.key, + textNode: context.node, + editorState: context.editorState, ); } + + @override + NodeValidator get nodeValidator => ((node) { + return true; + }); } class BulletedListTextNodeWidget extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index cdadcfe08c..4d52e41867 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -2,30 +2,27 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/infra/flowy_svg.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/node_widget_builder.dart'; -import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flowy_editor/render/rich_text/default_selectable.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; -class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder { - CheckboxNodeWidgetBuilder.create({ - required super.editorState, - required super.node, - required super.key, - }) : super.create(); - +class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder { @override - Widget build(BuildContext context) { + Widget build(NodeWidgetContext context) { return CheckboxNodeWidget( - key: key, - textNode: node as TextNode, - editorState: editorState, + key: context.node.key, + textNode: context.node, + editorState: context.editorState, ); } + + @override + NodeValidator get nodeValidator => ((node) { + return node.attributes.containsKey(StyleKey.check); + }); } class CheckboxNodeWidget extends StatefulWidget { @@ -67,7 +64,7 @@ class _CheckboxNodeWidgetState extends State } Widget _buildWithSingle(BuildContext context) { - final check = widget.textNode.attributes.checkbox; + final check = widget.textNode.attributes.check; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -107,9 +104,10 @@ class _CheckboxNodeWidgetState extends State Column( children: widget.textNode.children .map( - (child) => widget.editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, + (child) => widget.editorState.service.renderPluginService + .buildPluginWidget( + NodeWidgetContext( + context: context, node: child, editorState: widget.editorState, ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index d0f96372c3..70834184cc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -4,29 +4,27 @@ import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/render/node_widget_builder.dart'; -import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { - RichTextNodeWidgetBuilder.create({ - required super.editorState, - required super.node, - required super.key, - }) : super.create(); - +class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { @override - Widget build(BuildContext context) { + Widget build(NodeWidgetContext context) { return FlowyRichText( - key: key, - textNode: node as TextNode, - editorState: editorState, + key: context.node.key, + textNode: context.node, + editorState: context.editorState, ); } + + @override + NodeValidator get nodeValidator => ((node) { + return true; + }); } typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); @@ -152,9 +150,10 @@ class _FlowyRichTextState extends State with Selectable { _buildSingleRichText(context), ...widget.textNode.children .map( - (child) => widget.editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, + (child) => widget.editorState.service.renderPluginService + .buildPluginWidget( + NodeWidgetContext( + context: context, node: child, editorState: widget.editorState, ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart index 0453ac6952..4990e90dcf 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart @@ -1,27 +1,26 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/render/node_widget_builder.dart'; import 'package:flowy_editor/render/rich_text/default_selectable.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; -class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder { - HeadingTextNodeWidgetBuilder.create({ - required super.editorState, - required super.node, - required super.key, - }) : super.create(); - +class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder { @override - Widget build(BuildContext context) { + Widget build(NodeWidgetContext context) { return HeadingTextNodeWidget( - key: key, - textNode: node as TextNode, - editorState: editorState, + key: context.node.key, + textNode: context.node, + editorState: context.editorState, ); } + + @override + NodeValidator get nodeValidator => ((node) { + return node.attributes.heading != null; + }); } class HeadingTextNodeWidget extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart index 288199f797..1c52b93d4b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart @@ -1,28 +1,27 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/render/node_widget_builder.dart'; import 'package:flowy_editor/render/rich_text/default_selectable.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; -class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder { - NumberListTextNodeWidgetBuilder.create({ - required super.editorState, - required super.node, - required super.key, - }) : super.create(); - +class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder { @override - Widget build(BuildContext context) { + Widget build(NodeWidgetContext context) { return NumberListTextNodeWidget( - key: key, - textNode: node as TextNode, - editorState: editorState, + key: context.node.key, + textNode: context.node, + editorState: context.editorState, ); } + + @override + NodeValidator get nodeValidator => ((node) { + return node.attributes.number != null; + }); } class NumberListTextNodeWidget extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart index 8d0c919d7b..41520c560f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart @@ -1,27 +1,27 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/render/node_widget_builder.dart'; import 'package:flowy_editor/render/rich_text/default_selectable.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; -class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder { - QuotedTextNodeWidgetBuilder.create({ - required super.editorState, - required super.node, - required super.key, - }) : super.create(); - +class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder { @override - Widget build(BuildContext context) { + Widget build(NodeWidgetContext context) { return QuotedTextNodeWidget( - key: key, - textNode: node as TextNode, - editorState: editorState, + key: context.node.key, + textNode: context.node, + editorState: context.editorState, ); } + + @override + NodeValidator get nodeValidator => ((node) { + return true; + }); } class QuotedTextNodeWidget extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index 3c11feb20d..26d4275774 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -32,7 +32,7 @@ class StyleKey { static String code = 'code'; static String subtype = 'subtype'; - static String checkbox = 'checkbox'; + static String check = 'checkbox'; static String heading = 'heading'; } @@ -63,10 +63,7 @@ extension NodeAttributesExtensions on Attributes { } bool get quote { - if (containsKey(StyleKey.quote) && this[StyleKey.quote] == true) { - return this[StyleKey.quote]; - } - return false; + return containsKey(StyleKey.quote); } Color? get quoteColor { @@ -104,9 +101,9 @@ extension NodeAttributesExtensions on Attributes { return false; } - bool get checkbox { - if (containsKey(StyleKey.checkbox) && this[StyleKey.checkbox] is bool) { - return this[StyleKey.checkbox]; + bool get check { + if (containsKey(StyleKey.check) && this[StyleKey.check] is bool) { + return this[StyleKey.check]; } return false; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 68f08ac4ce..f01cbe8a28 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,6 +1,10 @@ +import 'package:flowy_editor/render/editor/editor_entry.dart'; +import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; import 'package:flowy_editor/service/input_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; @@ -9,21 +13,41 @@ import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handle import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; +import 'package:flowy_editor/render/rich_text/heading_text.dart'; +import 'package:flowy_editor/render/rich_text/number_list_text.dart'; +import 'package:flowy_editor/render/rich_text/quoted_text.dart'; import 'package:flutter/material.dart'; +NodeWidgetBuilders defaultBuilders = { + 'editor': EditorEntryWidgetBuilder(), + 'text': RichTextNodeWidgetBuilder(), + 'text/checkbox': CheckboxNodeWidgetBuilder(), + 'text/heading': HeadingTextNodeWidgetBuilder(), + 'text/bullet-list': BulletedListTextNodeWidgetBuilder(), + 'text/number-list': NumberListTextNodeWidgetBuilder(), + 'text/quote': QuotedTextNodeWidgetBuilder(), +}; + class FlowyEditor extends StatefulWidget { const FlowyEditor({ Key? key, required this.editorState, - required this.keyEventHandlers, - required this.shortcuts, + this.customBuilders = const {}, + this.keyEventHandlers = const [], + this.shortcuts = const [], }) : super(key: key); final EditorState editorState; + + /// Render plugins. + final NodeWidgetBuilders customBuilders; + + /// Keyboard event handlers. final List keyEventHandlers; - /// shortcuts + /// Shortcuts final FloatingShortcuts shortcuts; @override @@ -33,6 +57,19 @@ class FlowyEditor extends StatefulWidget { class _FlowyEditorState extends State { EditorState get editorState => widget.editorState; + @override + void initState() { + super.initState(); + + editorState.service.renderPluginService = FlowyRenderPlugin( + editorState: editorState, + builders: { + ...defaultBuilders, + ...widget.customBuilders, + }, + ); + } + @override Widget build(BuildContext context) { return FlowySelection( @@ -57,7 +94,13 @@ class _FlowyEditorState extends State { size: const Size(200, 150), // TODO: support customize size. editorState: editorState, floatingShortcuts: widget.shortcuts, - child: editorState.build(context), + child: editorState.service.renderPluginService.buildPluginWidget( + NodeWidgetContext( + context: context, + node: editorState.document.root, + editorState: editorState, + ), + ), ), ), ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart new file mode 100644 index 0000000000..47159097b5 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart @@ -0,0 +1,131 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +typedef NodeValidator = bool Function(T node); + +abstract class NodeWidgetBuilder { + NodeValidator get nodeValidator; + + Widget build(NodeWidgetContext context); +} + +typedef NodeWidgetBuilders = Map; + +abstract class FlowyRenderPluginService { + /// Register render plugin with specified [name]. + /// + /// [name] should be [Node].type + /// or [Node].type + '/' + [Node].attributes['subtype']. + /// + /// e.g. 'text', 'text/checkbox', or 'text/heading' + /// + /// [name] could be empty. + void register(String name, NodeWidgetBuilder builder); + void registerAll(Map builders); + + /// UnRegister plugin with specified [name]. + void unRegister(String name); + + Widget buildPluginWidget(NodeWidgetContext context); +} + +class NodeWidgetContext { + final BuildContext context; + final T node; + final EditorState editorState; + + NodeWidgetContext({ + required this.context, + required this.node, + required this.editorState, + }); + + NodeWidgetContext copyWith({ + BuildContext? context, + T? node, + EditorState? editorState, + }) { + return NodeWidgetContext( + context: context ?? this.context, + node: node ?? this.node, + editorState: editorState ?? this.editorState, + ); + } +} + +class FlowyRenderPlugin extends FlowyRenderPluginService { + FlowyRenderPlugin({ + required this.editorState, + required NodeWidgetBuilders builders, + }) { + registerAll(builders); + } + + final NodeWidgetBuilders _builders = {}; + final EditorState editorState; + + @override + Widget buildPluginWidget(NodeWidgetContext context) { + final node = context.node; + final name = + node.subtype == null ? node.type : '${node.type}/${node.subtype!}'; + final builder = _builders[name]; + if (builder != null && builder.nodeValidator(node)) { + final key = GlobalKey(debugLabel: name); + node.key = key; + return _wrap( + builder.build(context), + context, + ); + } else { + assert(false, 'Could not query the builder with this $name'); + // TODO: return a placeholder widget with tips. + return Container(); + } + } + + @override + void register(String name, NodeWidgetBuilder builder) { + debugPrint('[Plugins] registering $name...'); + _validatePlugin(name); + _builders[name] = builder; + } + + @override + void registerAll(Map> builders) { + builders.forEach(register); + } + + @override + void unRegister(String name) { + _validatePlugin(name); + _builders.remove(name); + } + + Widget _wrap(Widget widget, NodeWidgetContext context) { + return CompositedTransformTarget( + link: context.node.layerLink, + child: ChangeNotifierProvider.value( + value: context.node, + builder: (context, child) => Consumer( + builder: ((context, value, child) { + debugPrint('Node is rebuilding...'); + return widget; + }), + ), + ), + ); + } + + void _validatePlugin(String name) { + final paths = name.split('/'); + if (paths.length > 2) { + throw Exception('Plugin name must contain at most one or zero slash'); + } + if (_builders.containsKey(name)) { + throw Exception('Plugin name($name) already exists.'); + } + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 8fe715bbe7..551002499d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; import 'package:flutter/material.dart'; @@ -17,6 +18,9 @@ class FlowyService { // input service final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service'); + // render plugin service + late FlowyRenderPlugin renderPluginService; + // floating shortcut service final floatingShortcutServiceKey = GlobalKey(debugLabel: 'flowy_floating_shortcut_service'); diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index 176f00b734..b0b6cec141 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -6,7 +6,6 @@ import 'package:flowy_editor/operation/operation.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/state_tree.dart'; -import 'package:flowy_editor/render/render_plugins.dart'; void main() { group('transform path', () { @@ -64,8 +63,7 @@ void main() { item2, item3, ])); - final state = EditorState( - document: StateTree(root: root), renderPlugins: RenderPlugins()); + final state = EditorState(document: StateTree(root: root)); expect(item1.path, [0]); expect(item2.path, [1]); From 966eea21791077231d0454ef05e66ef28af15bb6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 15:36:17 +0800 Subject: [PATCH 091/121] chore: format code --- .../flowy_editor/example/assets/example.json | 11 ---- .../lib/service/editor_service.dart | 53 +++++++++++-------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 2e982f98e4..6a0fba3021 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -3,17 +3,6 @@ "type": "editor", "attributes": {}, "children": [ - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "quote" - } - }, { "type": "image", "attributes": { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index f01cbe8a28..1eceb099f2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,24 +1,24 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/render/editor/editor_entry.dart'; +import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; -import 'package:flowy_editor/service/input_service.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/shortcut_service.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/service/selection_service.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/render/rich_text/number_list_text.dart'; import 'package:flowy_editor/render/rich_text/quoted_text.dart'; - -import 'package:flutter/material.dart'; +import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; +import 'package:flowy_editor/service/input_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/service/selection_service.dart'; +import 'package:flowy_editor/service/shortcut_service.dart'; NodeWidgetBuilders defaultBuilders = { 'editor': EditorEntryWidgetBuilder(), @@ -61,13 +61,14 @@ class _FlowyEditorState extends State { void initState() { super.initState(); - editorState.service.renderPluginService = FlowyRenderPlugin( - editorState: editorState, - builders: { - ...defaultBuilders, - ...widget.customBuilders, - }, - ); + editorState.service.renderPluginService = _createRenderPlugin(); + } + + @override + void didUpdateWidget(covariant FlowyEditor oldWidget) { + super.didUpdateWidget(oldWidget); + + editorState.service.renderPluginService = _createRenderPlugin(); } @override @@ -106,4 +107,12 @@ class _FlowyEditorState extends State { ), ); } + + FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin( + editorState: editorState, + builders: { + ...defaultBuilders, + ...widget.customBuilders, + }, + ); } From c4b3c54a7c117db07053d4f44743b703d3f3c408 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 15:45:49 +0800 Subject: [PATCH 092/121] chore: format code --- .../lib/service/editor_service.dart | 18 ++++++++++++------ .../lib/service/input_service.dart | 7 ++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 1eceb099f2..de5667fa10 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -30,6 +30,14 @@ NodeWidgetBuilders defaultBuilders = { 'text/quote': QuotedTextNodeWidgetBuilder(), }; +List defaultKeyEventHandler = [ + slashShortcutHandler, + flowyDeleteNodesHandler, + deleteSingleTextNodeHandler, + arrowKeysHandler, + enterInEdgeOfTextNodeHandler, +]; + class FlowyEditor extends StatefulWidget { const FlowyEditor({ Key? key, @@ -68,7 +76,9 @@ class _FlowyEditorState extends State { void didUpdateWidget(covariant FlowyEditor oldWidget) { super.didUpdateWidget(oldWidget); - editorState.service.renderPluginService = _createRenderPlugin(); + if (editorState.service != oldWidget.editorState.service) { + editorState.service.renderPluginService = _createRenderPlugin(); + } } @override @@ -82,11 +92,7 @@ class _FlowyEditorState extends State { child: FlowyKeyboard( key: editorState.service.keyboardServiceKey, handlers: [ - slashShortcutHandler, - flowyDeleteNodesHandler, - deleteSingleTextNodeHandler, - arrowKeysHandler, - enterInEdgeOfTextNodeHandler, + ...defaultKeyEventHandler, ...widget.keyEventHandlers, ], editorState: editorState, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart index 38309414f4..9bc35f10ab 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -1,10 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; mixin FlowyInputService { void attach(TextEditingValue textEditingValue); From 55d46edeaf845eeb9a34c6f4f70a12c34466d66d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 23:16:56 +0800 Subject: [PATCH 093/121] fix: node change notifier doesn't work --- .../flowy_editor/example/lib/main.dart | 2 + .../lib/service/editor_service.dart | 2 - .../lib/service/input_service.dart | 22 ++++++ .../delete_single_text_node_handler.dart | 69 ------------------- .../lib/service/render_plugin_service.dart | 46 ++++++++----- 5 files changed, 54 insertions(+), 87 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 7ebb340f2d..a2fcc7a4df 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -55,6 +55,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { late EditorState _editorState; + final editorKey = GlobalKey(); int page = 0; @override @@ -116,6 +117,7 @@ class _MyHomePageState extends State { document: document, ); return FlowyEditor( + key: editorKey, editorState: _editorState, keyEventHandlers: const [], customBuilders: { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index de5667fa10..1d961737ff 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -12,7 +12,6 @@ import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; import 'package:flowy_editor/service/input_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; @@ -33,7 +32,6 @@ NodeWidgetBuilders defaultBuilders = { List defaultKeyEventHandler = [ slashShortcutHandler, flowyDeleteNodesHandler, - deleteSingleTextNodeHandler, arrowKeysHandler, enterInEdgeOfTextNodeHandler, ]; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart index 9bc35f10ab..a28d572d30 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -94,6 +94,7 @@ class _FlowyInputState extends State // TODO: implement the detail for (final delta in deltas) { if (delta is TextEditingDeltaInsertion) { + _applyInsert(delta); } else if (delta is TextEditingDeltaDeletion) { } else if (delta is TextEditingDeltaReplacement) { } else if (delta is TextEditingDeltaNonTextUpdate) { @@ -103,6 +104,27 @@ class _FlowyInputState extends State } } + void _applyInsert(TextEditingDeltaInsertion delta) { + final selectionService = _editorState.service.selectionService; + final currentSelection = selectionService.currentSelection; + if (currentSelection == null) { + return; + } + if (currentSelection.isSingle) { + final textNode = + selectionService.currentSelectedNodes.value.first as TextNode; + TransactionBuilder(_editorState) + ..insertText( + textNode, + delta.insertionOffset, + delta.textInserted, + ) + ..commit(); + } else { + // TODO: implement + } + } + @override void close() { _textInputConnection?.close(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart deleted file mode 100644 index f5da6423ae..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/extensions/object_extensions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -// TODO: need to be refactored, just a example code. -FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.backspace) { - return KeyEventResult.ignored; - } - - // final selectionNodes = editorState.selectedNodes; - // if (selectionNodes.length == 1 && selectionNodes.first is TextNode) { - // final node = selectionNodes.first.unwrapOrNull(); - // final selectable = node?.key?.currentState?.unwrapOrNull(); - // if (selectable != null) { - // final textSelection = selectable.getCurrentTextSelection(); - // if (textSelection != null) { - // if (textSelection.isCollapsed) { - // /// Three cases: - // /// Delete the zero character, - // /// 1. if there is still text node in front of it, then merge them. - // /// 2. if not, just ignore - // /// Delete the non-zero character, - // /// 3. delete the single character. - // if (textSelection.baseOffset == 0) { - // if (node?.previous != null && node?.previous is TextNode) { - // final previous = node!.previous! as TextNode; - // final newTextSelection = TextSelection.collapsed( - // offset: previous.toRawString().length); - // final selectionService = editorState.service.selectionService; - // final previousSelectable = - // previous.key?.currentState?.unwrapOrNull(); - // final newOfset = previousSelectable - // ?.getOffsetByTextSelection(newTextSelection); - // if (newOfset != null) { - // // selectionService.updateCursor(newOfset); - // } - // // merge - // TransactionBuilder(editorState) - // ..deleteNode(node) - // ..insertText( - // previous, previous.toRawString().length, node.toRawString()) - // ..commit(); - // return KeyEventResult.handled; - // } else { - // return KeyEventResult.ignored; - // } - // } else { - // TransactionBuilder(editorState) - // ..deleteText(node!, textSelection.baseOffset - 1, 1) - // ..commit(); - // final newTextSelection = - // TextSelection.collapsed(offset: textSelection.baseOffset - 1); - // final selectionService = editorState.service.selectionService; - // final newOfset = - // selectable.getOffsetByTextSelection(newTextSelection); - // // selectionService.updateCursor(newOfset); - // return KeyEventResult.handled; - // } - // } - // } - // } - // } - return KeyEventResult.ignored; -}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart index 47159097b5..8ac32ac66c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart @@ -75,10 +75,7 @@ class FlowyRenderPlugin extends FlowyRenderPluginService { if (builder != null && builder.nodeValidator(node)) { final key = GlobalKey(debugLabel: name); node.key = key; - return _wrap( - builder.build(context), - context, - ); + return _autoUpdateNodeWidget(builder, context); } else { assert(false, 'Could not query the builder with this $name'); // TODO: return a placeholder widget with tips. @@ -87,14 +84,14 @@ class FlowyRenderPlugin extends FlowyRenderPluginService { } @override - void register(String name, NodeWidgetBuilder builder) { + void register(String name, NodeWidgetBuilder builder) { debugPrint('[Plugins] registering $name...'); _validatePlugin(name); _builders[name] = builder; } @override - void registerAll(Map> builders) { + void registerAll(Map builders) { builders.forEach(register); } @@ -104,18 +101,35 @@ class FlowyRenderPlugin extends FlowyRenderPluginService { _builders.remove(name); } - Widget _wrap(Widget widget, NodeWidgetContext context) { + Widget _autoUpdateNodeWidget( + NodeWidgetBuilder builder, NodeWidgetContext context) { + Widget notifier; + if (context.node is TextNode) { + notifier = ChangeNotifierProvider.value( + value: context.node as TextNode, + builder: (_, child) { + return Consumer( + builder: ((_, value, child) { + debugPrint('Text Node is rebuilding...'); + return builder.build(context); + }), + ); + }); + } else { + notifier = ChangeNotifierProvider.value( + value: context.node, + builder: (_, child) { + return Consumer( + builder: ((_, value, child) { + debugPrint('Node is rebuilding...'); + return builder.build(context); + }), + ); + }); + } return CompositedTransformTarget( link: context.node.layerLink, - child: ChangeNotifierProvider.value( - value: context.node, - builder: (context, child) => Consumer( - builder: ((context, value, child) { - debugPrint('Node is rebuilding...'); - return widget; - }), - ), - ), + child: notifier, ); } From 575e01c9094c9e9e01ef2c3fe804ddda540b7491 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 23:27:39 +0800 Subject: [PATCH 094/121] feat: implement text replacement in singe selection --- .../lib/operation/transaction_builder.dart | 13 ++++++++++++ .../lib/service/input_service.dart | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index fb042fe566..64fede87b3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -75,6 +75,19 @@ class TransactionBuilder { Selection.collapsed(Position(path: node.path, offset: index)); } + replaceText(TextNode node, int index, int length, String content) { + textEdit( + node, + () => Delta().retain(index).delete(length).insert(content), + ); + afterSelection = Selection.collapsed( + Position( + path: node.path, + offset: index + content.length, + ), + ); + } + add(Operation op) { final Operation? last = operations.isEmpty ? null : operations.last; if (last != null) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart index a28d572d30..ee570d902a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -97,6 +97,7 @@ class _FlowyInputState extends State _applyInsert(delta); } else if (delta is TextEditingDeltaDeletion) { } else if (delta is TextEditingDeltaReplacement) { + _applyReplacement(delta); } else if (delta is TextEditingDeltaNonTextUpdate) { // We don't need to care the [TextEditingDeltaNonTextUpdate]. // Do nothing. @@ -125,6 +126,25 @@ class _FlowyInputState extends State } } + void _applyReplacement(TextEditingDeltaReplacement delta) { + final selectionService = _editorState.service.selectionService; + final currentSelection = selectionService.currentSelection; + if (currentSelection == null) { + return; + } + if (currentSelection.isSingle) { + final textNode = + selectionService.currentSelectedNodes.value.first as TextNode; + final length = delta.replacedRange.end - delta.replacedRange.start; + TransactionBuilder(_editorState) + ..replaceText( + textNode, delta.replacedRange.start, length, delta.replacementText) + ..commit(); + } else { + // TODO: implement + } + } + @override void close() { _textInputConnection?.close(); From b245841ec3ac4ee89300a34b74033a12b93b1c13 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 30 Jul 2022 00:00:10 +0800 Subject: [PATCH 095/121] feat: implement text delete --- .../flowy_editor/example/assets/example.json | 6 +- .../lib/service/editor_service.dart | 2 + .../delele_text_handler.dart | 83 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 6a0fba3021..9f1d278e16 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -162,7 +162,11 @@ "type": "text", "delta": [ { - "insert": "Hello world" + "insert": "Hello " + }, + { + "insert": "world", + "attributes": { "bold": true } } ], "attributes": { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 1d961737ff..b7a64ffcc7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/service/internal_key_event_handlers/delele_text_handler.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/editor_state.dart'; @@ -30,6 +31,7 @@ NodeWidgetBuilders defaultBuilders = { }; List defaultKeyEventHandler = [ + deleteTextHandler, slashShortcutHandler, flowyDeleteNodesHandler, arrowKeysHandler, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart new file mode 100644 index 0000000000..b50d947d5c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart @@ -0,0 +1,83 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Handle delete text. +FlowyKeyEventHandler deleteTextHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.backspace) { + return KeyEventResult.ignored; + } + + final selection = editorState.service.selectionService.currentSelection; + if (selection == null) { + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + // make sure all nodes is [TextNode]. + final textNodes = nodes.whereType().toList(); + if (textNodes.length != nodes.length) { + return KeyEventResult.ignored; + } + + TransactionBuilder transactionBuilder = TransactionBuilder(editorState); + if (textNodes.length == 1) { + final textNode = textNodes.first; + final index = selection.start.offset - 1; + if (index < 0) { + // 1. style + if (textNode.subtype != null) { + transactionBuilder.updateNode(textNode, { + 'subtype': null, + }); + } else { + // 2. non-style + // find previous text node. + while (textNode.previous != null) { + if (textNode.previous is TextNode) { + final previous = textNode.previous as TextNode; + transactionBuilder + ..deleteNode(textNode) + ..insertText( + previous, + previous.toRawString().length, + textNode.toRawString(), + ); + // FIXME: keep the attributes. + break; + } + } + } + } else { + transactionBuilder.deleteText( + textNode, + selection.start.offset - 1, + 1, + ); + } + } else { + for (var i = 0; i < textNodes.length; i++) { + final textNode = textNodes[i]; + if (i == 0) { + transactionBuilder.deleteText( + textNode, + selection.start.offset, + textNode.toRawString().length - selection.start.offset, + ); + } else if (i == textNodes.length - 1) { + transactionBuilder.deleteText( + textNode, + 0, + selection.end.offset, + ); + } else { + transactionBuilder.deleteNode(textNode); + } + } + } + + transactionBuilder.commit(); + + return KeyEventResult.handled; +}; From 29fe4811c347cacf5501767d2e726e1505c56767 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 31 Jul 2022 15:57:22 +0800 Subject: [PATCH 096/121] fix: selection areas could not overlay --- ...{flowy_selection_widget.dart => selection_widget.dart} | 8 ++++++-- .../flowy_editor/lib/service/selection_service.dart | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) rename frontend/app_flowy/packages/flowy_editor/lib/render/selection/{flowy_selection_widget.dart => selection_widget.dart} (76%) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart similarity index 76% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart index 96dd6a7759..e3dea7af34 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart @@ -25,8 +25,12 @@ class _SelectionWidgetState extends State { link: widget.layerLink, offset: widget.rect.topLeft, showWhenUnlinked: true, - child: Container( - color: widget.color, + // Ignore the gestures in selection overlays + // to solve the problem that selection areas cannot overlap. + child: IgnorePointer( + child: Container( + color: widget.color, + ), ), ), ); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 975677d508..375ebee31c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -5,7 +5,7 @@ import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/render/selection/cursor_widget.dart'; -import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; +import 'package:flowy_editor/render/selection/selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flutter/gestures.dart'; From 89a0a5599e40e15bcfaf6918552ad073860a6e72 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 31 Jul 2022 16:01:46 +0800 Subject: [PATCH 097/121] fix: cursor cannot be selected in same position. --- .../flowy_editor/lib/render/selection/cursor_widget.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart index 6a27eed855..19da4b55f4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart @@ -64,8 +64,12 @@ class CursorWidgetState extends State { link: widget.layerLink, offset: widget.rect.topCenter, showWhenUnlinked: true, - child: Container( - color: showCursor ? widget.color : Colors.transparent, + // Ignore the gestures in cursor + // to solve the problem that cursor area cannot be selected. + child: IgnorePointer( + child: Container( + color: showCursor ? widget.color : Colors.transparent, + ), ), ), ); From b577489c2f04b406a5fe30c69b5f3b0d9217f3ed Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 31 Jul 2022 16:14:12 +0800 Subject: [PATCH 098/121] feat: implement delete multiple text node and merge the text. --- .../lib/operation/transaction_builder.dart | 4 +++ .../delele_text_handler.dart | 31 ++++++++----------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 64fede87b3..5c50a19c42 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -48,6 +48,10 @@ class TransactionBuilder { add(DeleteOperation(path: node.path, removedValue: node)); } + deleteNodes(List nodes) { + nodes.forEach(deleteNode); + } + textEdit(TextNode node, Delta Function() f) { beforeSelection = state.cursorSelection; final path = node.path; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart index b50d947d5c..601c0ffef6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart @@ -57,24 +57,19 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { ); } } else { - for (var i = 0; i < textNodes.length; i++) { - final textNode = textNodes[i]; - if (i == 0) { - transactionBuilder.deleteText( - textNode, - selection.start.offset, - textNode.toRawString().length - selection.start.offset, - ); - } else if (i == textNodes.length - 1) { - transactionBuilder.deleteText( - textNode, - 0, - selection.end.offset, - ); - } else { - transactionBuilder.deleteNode(textNode); - } - } + final first = textNodes.first; + var content = textNodes.last.toRawString(); + content = content.substring(selection.end.offset, content.length); + // Merge the fist and the last text node content, + // and delete the all nodes expect for the first. + transactionBuilder + ..deleteNodes(textNodes.sublist(1)) + ..replaceText( + first, + selection.start.offset, + first.toRawString().length - selection.start.offset, + content, + ); } transactionBuilder.commit(); From d058f2d5914b39afb50c4fa6bb70369e9fef0f60 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 31 Jul 2022 17:16:07 +0800 Subject: [PATCH 099/121] feat: bold the text by command/control + b/B --- .../lib/service/editor_service.dart | 2 + ...pdate_text_style_by_command_x_handler.dart | 83 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index b7a64ffcc7..71143a3ede 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,4 +1,5 @@ import 'package:flowy_editor/service/internal_key_event_handlers/delele_text_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/editor_state.dart'; @@ -36,6 +37,7 @@ List defaultKeyEventHandler = [ flowyDeleteNodesHandler, arrowKeysHandler, enterInEdgeOfTextNodeHandler, + updateTextStyleByCommandXHandler, ]; class FlowyEditor extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart new file mode 100644 index 0000000000..5f13484442 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -0,0 +1,83 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flutter/material.dart'; + +FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { + if (!event.isMetaPressed || event.character == null) { + return KeyEventResult.ignored; + } + + final selection = editorState.service.selectionService.currentSelection; + final nodes = editorState.service.selectionService.currentSelectedNodes.value + .whereType() + .toList(); + + if (selection == null || nodes.isEmpty) { + return KeyEventResult.ignored; + } + + switch (event.character!) { + // bold + case 'B': + case 'b': + _makeBold(editorState, nodes, selection); + return KeyEventResult.handled; + default: + break; + } + + return KeyEventResult.ignored; +}; + +// TODO: implement unBold. +void _makeBold( + EditorState editorState, List nodes, Selection selection) { + final builder = TransactionBuilder(editorState); + if (nodes.length == 1) { + builder.formatText( + nodes.first, + selection.start.offset, + selection.end.offset - selection.start.offset, + { + 'bold': true, + }, + ); + } else { + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + if (i == 0) { + builder.formatText( + node, + selection.start.offset, + node.toRawString().length - selection.start.offset, + { + 'bold': true, + }, + ); + } else if (i == nodes.length - 1) { + builder.formatText( + node, + 0, + selection.end.offset, + { + 'bold': true, + }, + ); + } else { + builder.formatText( + node, + 0, + node.toRawString().length, + { + 'bold': true, + }, + ); + } + } + } + builder.commit(); +} From c65f2e1b38b32674fbcaf17536d5cf5e4363bd84 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 1 Aug 2022 10:42:38 +0800 Subject: [PATCH 100/121] fix: delete text in single line --- .../delele_text_handler.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart index 601c0ffef6..4ad34b3c08 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart @@ -50,11 +50,19 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { } } } else { - transactionBuilder.deleteText( - textNode, - selection.start.offset - 1, - 1, - ); + if (selection.isCollapsed) { + transactionBuilder.deleteText( + textNode, + selection.start.offset - 1, + 1, + ); + } else { + transactionBuilder.deleteText( + textNode, + selection.start.offset, + selection.end.offset - selection.start.offset, + ); + } } } else { final first = textNodes.first; From 58856ccb1ec1b3162d2dd6cb18d400fade58fa43 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 1 Aug 2022 11:29:04 +0800 Subject: [PATCH 101/121] feat: implement deleting text in multiple lines. --- .../flowy_editor/lib/document/text_delta.dart | 5 ++++ .../lib/operation/transaction_builder.dart | 24 +++++++++++++++++-- .../delele_text_handler.dart | 16 +++++-------- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index 2f3d194255..64335d4a05 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -275,6 +275,11 @@ class Delta { Delta([List? ops]) : operations = ops ?? []; + Delta addAll(List textOps) { + textOps.forEach(add); + return this; + } + Delta add(TextOperation textOp) { if (textOp.isEmpty) { return this; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 5c50a19c42..e70dfc411a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -63,8 +63,28 @@ class TransactionBuilder { add(TextEditOperation(path: path, delta: delta, inverted: inverted)); } - insertText(TextNode node, int index, String content) { - textEdit(node, () => Delta().retain(index).insert(content)); + mergeText(TextNode firstNode, TextNode secondNode, + {int? firstOffset, int secondOffset = 0}) { + final firstLength = firstNode.delta.length; + final secondLength = secondNode.delta.length; + textEdit( + firstNode, + () => Delta() + ..retain(firstOffset ?? firstLength) + ..delete(firstLength - (firstOffset ?? firstLength)) + ..addAll(secondNode.delta.slice(secondOffset, secondLength).operations), + ); + afterSelection = Selection.collapsed( + Position( + path: firstNode.path, + offset: firstOffset ?? firstLength, + ), + ); + } + + insertText(TextNode node, int index, String content, + [Attributes? attributes]) { + textEdit(node, () => Delta().retain(index).insert(content, attributes)); afterSelection = Selection.collapsed( Position(path: node.path, offset: index + content.length)); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart index 4ad34b3c08..498fd845b2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart @@ -39,12 +39,7 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { final previous = textNode.previous as TextNode; transactionBuilder ..deleteNode(textNode) - ..insertText( - previous, - previous.toRawString().length, - textNode.toRawString(), - ); - // FIXME: keep the attributes. + ..mergeText(previous, textNode); break; } } @@ -66,17 +61,18 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { } } else { final first = textNodes.first; + final last = textNodes.last; var content = textNodes.last.toRawString(); content = content.substring(selection.end.offset, content.length); // Merge the fist and the last text node content, // and delete the all nodes expect for the first. transactionBuilder ..deleteNodes(textNodes.sublist(1)) - ..replaceText( + ..mergeText( first, - selection.start.offset, - first.toRawString().length - selection.start.offset, - content, + last, + firstOffset: selection.start.offset, + secondOffset: selection.end.offset, ); } From 2e2de297899a7332db607447fbd78a71323fce8c Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 1 Aug 2022 11:34:35 +0800 Subject: [PATCH 102/121] feat: transaction to json --- .../flowy_editor/lib/document/position.dart | 7 +++ .../flowy_editor/lib/document/selection.dart | 7 +++ .../flowy_editor/lib/operation/operation.dart | 39 ++++++++++++++++ .../lib/operation/transaction.dart | 13 ++++++ .../flowy_editor/test/operation_test.dart | 44 +++++++++++++++++++ 5 files changed, 110 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart index a60f04e89b..a87064d85a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart @@ -34,4 +34,11 @@ class Position { @override String toString() => 'path = $path, offset = $offset'; + + Map toJson() { + return { + "path": path.toList(), + "offset": offset, + }; + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart index a3919a21f6..f1fa0682f6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -48,4 +48,11 @@ class Selection { @override String toString() => '[Selection] start = $start, end = $end'; + + Map toJson() { + return { + "start": start.toJson(), + "end": end.toJson(), + }; + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index eafa4a31da..e4d2c1d8ed 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -6,6 +6,7 @@ abstract class Operation { Operation({required this.path}); Operation copyWithPath(Path path); Operation invert(); + Map toJson(); } class InsertOperation extends Operation { @@ -29,6 +30,15 @@ class InsertOperation extends Operation { removedValue: value, ); } + + @override + Map toJson() { + return { + "type": "insert-operation", + "path": path.toList(), + "value": value.toJson(), + }; + } } class UpdateOperation extends Operation { @@ -59,6 +69,16 @@ class UpdateOperation extends Operation { oldAttributes: attributes, ); } + + @override + Map toJson() { + return { + "type": "update-operation", + "path": path.toList(), + "attributes": {...attributes}, + "oldAttributes": {...oldAttributes}, + }; + } } class DeleteOperation extends Operation { @@ -82,6 +102,15 @@ class DeleteOperation extends Operation { value: removedValue, ); } + + @override + Map toJson() { + return { + "type": "delete-operation", + "path": path.toList(), + "removedValue": removedValue.toJson(), + }; + } } class TextEditOperation extends Operation { @@ -107,6 +136,16 @@ class TextEditOperation extends Operation { Operation invert() { return TextEditOperation(path: path, delta: inverted, inverted: delta); } + + @override + Map toJson() { + return { + "type": "text-edit-operation", + "path": path.toList(), + "delta": delta.toJson(), + "invert": inverted.toJson(), + }; + } } Path transformPath(Path preInsertPath, Path b, [int delta = 1]) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart index 85bc43f537..5dcf167628 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart @@ -23,4 +23,17 @@ class Transaction { this.beforeSelection, this.afterSelection, }); + + Map toJson() { + final Map result = { + "operations": operations.map((e) => e.toJson()), + }; + if (beforeSelection != null) { + result["beforeSelection"] = beforeSelection!.toJson(); + } + if (afterSelection != null) { + result["afterSelection"] = afterSelection!.toJson(); + } + return result; + } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index b0b6cec141..a027fa8027 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -78,4 +78,48 @@ void main() { expect(transaction.operations[1].path, [0]); expect(transaction.operations[2].path, [0]); }); + group("toJson", () { + test("insert", () { + final root = Node(type: "root", attributes: {}, children: LinkedList()); + final state = EditorState(document: StateTree(root: root)); + + final item1 = Node(type: "node", attributes: {}, children: LinkedList()); + final tb = TransactionBuilder(state); + tb.insertNode([0], item1); + + final transaction = tb.finish(); + expect(transaction.toJson(), { + "operations": [ + { + "type": "insert-operation", + "path": [0], + "value": item1.toJson(), + } + ], + }); + }); + test("delete", () { + final item1 = Node(type: "node", attributes: {}, children: LinkedList()); + final root = Node( + type: "root", + attributes: {}, + children: LinkedList() + ..addAll([ + item1, + ])); + final state = EditorState(document: StateTree(root: root)); + final tb = TransactionBuilder(state); + tb.deleteNode(item1); + final transaction = tb.finish(); + expect(transaction.toJson(), { + "operations": [ + { + "type": "delete-operation", + "path": [0], + "removedValue": item1.toJson(), + } + ], + }); + }); + }); } From 5e86b83eee8ae7395d2affde5692ef1b49b56bb6 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 1 Aug 2022 12:41:51 +0800 Subject: [PATCH 103/121] feat: fromJson --- .../packages/flowy_editor/lib/operation/operation.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index e4d2c1d8ed..d3b3144873 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -2,6 +2,16 @@ import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/flowy_editor.dart'; abstract class Operation { + factory Operation.fromJson(Map map) { + String t = map["type"] as String; + if (t == "insert-operation") { + final path = map["path"] as List; + final value = Node.fromJson(map["value"]); + return InsertOperation(path: path, value: value); + } + + throw ArgumentError('unexpected type $t'); + } final Path path; Operation({required this.path}); Operation copyWithPath(Path path); From 46dba122bde150812e608b50d91ffce5d342ff9d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 1 Aug 2022 12:58:32 +0800 Subject: [PATCH 104/121] refactor: remove named parameters --- .../flowy_editor/lib/operation/operation.dart | 104 +++++++++++------- .../lib/operation/transaction_builder.dart | 19 ++-- .../flowy_editor/test/operation_test.dart | 24 ++-- 3 files changed, 79 insertions(+), 68 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index d3b3144873..a505df4b1d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -5,15 +5,19 @@ abstract class Operation { factory Operation.fromJson(Map map) { String t = map["type"] as String; if (t == "insert-operation") { - final path = map["path"] as List; - final value = Node.fromJson(map["value"]); - return InsertOperation(path: path, value: value); + return InsertOperation.fromJson(map); + } else if (t == "update-operation") { + return UpdateOperation.fromJson(map); + } else if (t == "delete-operation") { + return DeleteOperation.fromJson(map); + } else if (t == "text-edit-operation") { + return TextEditOperation.fromJson(map); } throw ArgumentError('unexpected type $t'); } final Path path; - Operation({required this.path}); + Operation(this.path); Operation copyWithPath(Path path); Operation invert(); Map toJson(); @@ -22,13 +26,16 @@ abstract class Operation { class InsertOperation extends Operation { final Node value; - InsertOperation({ - required super.path, - required this.value, - }); + factory InsertOperation.fromJson(Map map) { + final path = map["path"] as List; + final value = Node.fromJson(map["value"]); + return InsertOperation(path, value); + } + + InsertOperation(Path path, this.value) : super(path); InsertOperation copyWith({Path? path, Node? value}) => - InsertOperation(path: path ?? this.path, value: value ?? this.value); + InsertOperation(path ?? this.path, value ?? this.value); @override Operation copyWithPath(Path path) => copyWith(path: path); @@ -36,8 +43,8 @@ class InsertOperation extends Operation { @override Operation invert() { return DeleteOperation( - path: path, - removedValue: value, + path, + value, ); } @@ -55,18 +62,23 @@ class UpdateOperation extends Operation { final Attributes attributes; final Attributes oldAttributes; - UpdateOperation({ - required super.path, - required this.attributes, - required this.oldAttributes, - }); + factory UpdateOperation.fromJson(Map map) { + final path = map["path"] as List; + final attributes = map["attributes"] as Map; + final oldAttributes = map["oldAttributes"] as Map; + return UpdateOperation(path, attributes, oldAttributes); + } + + UpdateOperation( + Path path, + this.attributes, + this.oldAttributes, + ) : super(path); UpdateOperation copyWith( {Path? path, Attributes? attributes, Attributes? oldAttributes}) => - UpdateOperation( - path: path ?? this.path, - attributes: attributes ?? this.attributes, - oldAttributes: oldAttributes ?? this.oldAttributes); + UpdateOperation(path ?? this.path, attributes ?? this.attributes, + oldAttributes ?? this.oldAttributes); @override Operation copyWithPath(Path path) => copyWith(path: path); @@ -74,9 +86,9 @@ class UpdateOperation extends Operation { @override Operation invert() { return UpdateOperation( - path: path, - attributes: oldAttributes, - oldAttributes: attributes, + path, + oldAttributes, + attributes, ); } @@ -94,23 +106,26 @@ class UpdateOperation extends Operation { class DeleteOperation extends Operation { final Node removedValue; - DeleteOperation({ - required super.path, - required this.removedValue, - }); + factory DeleteOperation.fromJson(Map map) { + final path = map["path"] as List; + final removedValue = Node.fromJson(map["removedValue"]); + return DeleteOperation(path, removedValue); + } - DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation( - path: path ?? this.path, removedValue: removedValue ?? this.removedValue); + DeleteOperation( + Path path, + this.removedValue, + ) : super(path); + + DeleteOperation copyWith({Path? path, Node? removedValue}) => + DeleteOperation(path ?? this.path, removedValue ?? this.removedValue); @override Operation copyWithPath(Path path) => copyWith(path: path); @override Operation invert() { - return InsertOperation( - path: path, - value: removedValue, - ); + return InsertOperation(path, removedValue); } @override @@ -127,24 +142,29 @@ class TextEditOperation extends Operation { final Delta delta; final Delta inverted; - TextEditOperation({ - required super.path, - required this.delta, - required this.inverted, - }); + factory TextEditOperation.fromJson(Map map) { + final path = map["path"] as List; + final delta = Delta.fromJson(map["delta"]); + final invert = Delta.fromJson(map["invert"]); + return TextEditOperation(path, delta, invert); + } + + TextEditOperation( + Path path, + this.delta, + this.inverted, + ) : super(path); TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) => TextEditOperation( - path: path ?? this.path, - delta: delta ?? this.delta, - inverted: inverted ?? this.inverted); + path ?? this.path, delta ?? this.delta, inverted ?? this.inverted); @override Operation copyWithPath(Path path) => copyWith(path: path); @override Operation invert() { - return TextEditOperation(path: path, delta: inverted, inverted: delta); + return TextEditOperation(path, inverted, delta); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index fb042fe566..2877e62588 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -1,5 +1,4 @@ import 'dart:collection'; -import 'dart:math'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/path.dart'; @@ -31,21 +30,21 @@ class TransactionBuilder { insertNode(Path path, Node node) { beforeSelection = state.cursorSelection; - add(InsertOperation(path: path, value: node)); + add(InsertOperation(path, node)); } updateNode(Node node, Attributes attributes) { beforeSelection = state.cursorSelection; add(UpdateOperation( - path: node.path, - attributes: Attributes.from(node.attributes)..addAll(attributes), - oldAttributes: node.attributes, + node.path, + Attributes.from(node.attributes)..addAll(attributes), + node.attributes, )); } deleteNode(Node node) { beforeSelection = state.cursorSelection; - add(DeleteOperation(path: node.path, removedValue: node)); + add(DeleteOperation(node.path, node)); } textEdit(TextNode node, Delta Function() f) { @@ -56,7 +55,7 @@ class TransactionBuilder { final inverted = delta.invert(node.delta); - add(TextEditOperation(path: path, delta: delta, inverted: inverted)); + add(TextEditOperation(path, delta, inverted)); } insertText(TextNode node, int index, String content) { @@ -82,9 +81,9 @@ class TransactionBuilder { last is TextEditOperation && pathEquals(op.path, last.path)) { final newOp = TextEditOperation( - path: op.path, - delta: last.delta.compose(op.delta), - inverted: op.inverted.compose(last.inverted), + op.path, + last.delta.compose(op.delta), + op.inverted.compose(last.inverted), ); operations[operations.length - 1] = newOp; return; diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index a027fa8027..c6811b86e2 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -27,26 +27,18 @@ void main() { group('transform operation', () { test('insert + insert', () { final t = transformOperation( - InsertOperation(path: [ - 0, - 1 - ], value: Node(type: "node", attributes: {}, children: LinkedList())), - InsertOperation( - path: [0, 1], - value: - Node(type: "node", attributes: {}, children: LinkedList()))); + InsertOperation([0, 1], + Node(type: "node", attributes: {}, children: LinkedList())), + InsertOperation([0, 1], + Node(type: "node", attributes: {}, children: LinkedList()))); expect(t.path, [0, 2]); }); test('delete + delete', () { final t = transformOperation( - DeleteOperation( - path: [0, 1], - removedValue: - Node(type: "node", attributes: {}, children: LinkedList())), - DeleteOperation( - path: [0, 2], - removedValue: - Node(type: "node", attributes: {}, children: LinkedList()))); + DeleteOperation([0, 1], + Node(type: "node", attributes: {}, children: LinkedList())), + DeleteOperation([0, 2], + Node(type: "node", attributes: {}, children: LinkedList()))); expect(t.path, [0, 1]); }); }); From 934cb6ab6b58afa4b1d445afb302c49a2a93b11d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 1 Aug 2022 17:41:11 +0800 Subject: [PATCH 105/121] feat: implement toolbar UI part. --- .../assets/images/toolbar/bold.svg | 3 + .../assets/images/toolbar/bulleted_list.svg | 8 + .../assets/images/toolbar/divider.svg | 3 + .../assets/images/toolbar/italic.svg | 3 + .../assets/images/toolbar/number_list.svg | 3 + .../assets/images/toolbar/quote.svg | 4 + .../assets/images/toolbar/strikethrough.svg | 4 + .../assets/images/toolbar/underline.svg | 4 + .../flowy_editor/example/lib/main.dart | 52 ++--- .../selection/floating_shortcut_widget.dart | 58 ----- .../lib/render/selection/toolbar_widget.dart | 217 ++++++++++++++++++ .../lib/service/editor_service.dart | 15 +- ..._handler.dart => delete_text_handler.dart} | 0 .../lib/service/selection_service.dart | 28 ++- .../flowy_editor/lib/service/service.dart | 14 +- .../lib/service/shortcut_service.dart | 60 ----- .../lib/service/toolbar_service.dart | 56 +++++ .../packages/flowy_editor/pubspec.yaml | 2 +- 18 files changed, 361 insertions(+), 173 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart rename frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/{delele_text_handler.dart => delete_text_handler.dart} (100%) delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg new file mode 100644 index 0000000000..85640695af --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg new file mode 100644 index 0000000000..c2c962fa0b --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg new file mode 100644 index 0000000000..3e57a6b000 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg new file mode 100644 index 0000000000..6b739a761f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg new file mode 100644 index 0000000000..2db0ab3b64 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg new file mode 100644 index 0000000000..8e55d9e2e3 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg new file mode 100644 index 0000000000..b37bb9acc0 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg new file mode 100644 index 0000000000..933471e6a7 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index a2fcc7a4df..158e33bbb1 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -123,32 +123,32 @@ class _MyHomePageState extends State { customBuilders: { 'image': ImageNodeBuilder(), }, - shortcuts: [ - // TODO: this won't work, just a example for now. - { - 'h1': (editorState, eventName) { - debugPrint('shortcut => $eventName'); - final selectedNodes = editorState.selectedNodes; - if (selectedNodes.isEmpty) { - return; - } - final textNode = selectedNodes.first as TextNode; - TransactionBuilder(editorState) - ..formatText(textNode, 0, textNode.toRawString().length, { - 'heading': 'h1', - }) - ..commit(); - } - }, - { - 'bold': (editorState, eventName) => - debugPrint('shortcut => $eventName') - }, - { - 'underline': (editorState, eventName) => - debugPrint('shortcut => $eventName') - }, - ], + // shortcuts: [ + // // TODO: this won't work, just a example for now. + // { + // 'h1': (editorState, eventName) { + // debugPrint('shortcut => $eventName'); + // final selectedNodes = editorState.selectedNodes; + // if (selectedNodes.isEmpty) { + // return; + // } + // final textNode = selectedNodes.first as TextNode; + // TransactionBuilder(editorState) + // ..formatText(textNode, 0, textNode.toRawString().length, { + // 'heading': 'h1', + // }) + // ..commit(); + // } + // }, + // { + // 'bold': (editorState, eventName) => + // debugPrint('shortcut => $eventName') + // }, + // { + // 'underline': (editorState, eventName) => + // debugPrint('shortcut => $eventName') + // }, + // ], ); } }, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart deleted file mode 100644 index 9fbbbbcb01..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/material.dart'; - -typedef FloatingShortcutHandler = void Function( - EditorState editorState, String eventName); -typedef FloatingShortcuts = List>; - -class FloatingShortcutWidget extends StatelessWidget { - const FloatingShortcutWidget({ - Key? key, - required this.editorState, - required this.layerLink, - required this.rect, - required this.floatingShortcuts, - }) : super(key: key); - - final EditorState editorState; - final LayerLink layerLink; - final Rect rect; - final FloatingShortcuts floatingShortcuts; - - List get _shortcutNames => - floatingShortcuts.map((shortcut) => shortcut.keys.first).toList(); - List get _shortcutHandlers => - floatingShortcuts.map((shortcut) => shortcut.values.first).toList(); - - @override - Widget build(BuildContext context) { - return Positioned.fromRect( - rect: rect, - child: CompositedTransformFollower( - link: layerLink, - offset: rect.topLeft, - showWhenUnlinked: true, - child: Container( - color: Colors.white, - child: ListView.builder( - itemCount: floatingShortcuts.length, - itemBuilder: ((context, index) { - final name = _shortcutNameInIndex(index); - final handler = _shortcutHandlerInIndex(index); - return Card( - child: GestureDetector( - onTap: () => handler(editorState, name), - child: ListTile(title: Text(name)), - ), - ); - }), - ), - ), - ), - ); - } - - String _shortcutNameInIndex(int index) => _shortcutNames[index]; - FloatingShortcutHandler _shortcutHandlerInIndex(int index) => - _shortcutHandlers[index]; -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart new file mode 100644 index 0000000000..7266929962 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart @@ -0,0 +1,217 @@ +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flutter/material.dart'; + +typedef ToolbarEventHandler = void Function( + EditorState editorState, String eventName); + +typedef ToolbarEventHandlers = List>; +ToolbarEventHandlers defaultToolbarEventHandlers = [ + { + 'bold': ((editorState, eventName) {}), + 'italic': ((editorState, eventName) {}), + 'strikethrough': ((editorState, eventName) {}), + 'underline': ((editorState, eventName) {}), + 'quote': ((editorState, eventName) {}), + 'number_list': ((editorState, eventName) {}), + 'bulleted_list': ((editorState, eventName) {}), + } +]; + +ToolbarEventHandlers defaultListToolbarEventHandlers = [ + { + 'h1': ((editorState, eventName) {}), + }, + { + 'h2': ((editorState, eventName) {}), + }, + { + 'h3': ((editorState, eventName) {}), + }, + { + 'bulleted_list': ((editorState, eventName) {}), + }, + { + 'quote': ((editorState, eventName) {}), + } +]; + +class ToolbarWidget extends StatefulWidget { + ToolbarWidget({ + Key? key, + required this.editorState, + required this.layerLink, + required this.offset, + required this.handlers, + }) : super(key: key); + + final EditorState editorState; + final LayerLink layerLink; + final Offset offset; + final ToolbarEventHandlers handlers; + + @override + State createState() => _ToolbarWidgetState(); +} + +class _ToolbarWidgetState extends State { + final GlobalKey _listToolbarKey = GlobalKey(); + + final toolbarHeight = 32.0; + final topPadding = 5.0; + + final listToolbarWidth = 60.0; + final listToolbarHeight = 120.0; + + final cornerRadius = 8.0; + + OverlayEntry? _listToolbarOverlay; + + @override + void initState() { + super.initState(); + + widget.editorState.service.selectionService.currentSelectedNodes + .addListener(_onSelectionChange); + } + + @override + void dispose() { + widget.editorState.service.selectionService.currentSelectedNodes + .removeListener(_onSelectionChange); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Positioned( + top: widget.offset.dx, + left: widget.offset.dy, + child: CompositedTransformFollower( + link: widget.layerLink, + showWhenUnlinked: true, + offset: widget.offset, + child: _buildToolbar(context), + ), + ); + } + + Widget _buildToolbar(BuildContext context) { + return Material( + borderRadius: BorderRadius.circular(cornerRadius), + color: const Color(0xFF333333), + child: SizedBox( + height: toolbarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _listToolbar(context), + _centerToolbarIcon('divider', width: 10), + _centerToolbarIcon('bold'), + _centerToolbarIcon('italic'), + _centerToolbarIcon('strikethrough'), + _centerToolbarIcon('underline'), + _centerToolbarIcon('divider', width: 10), + _centerToolbarIcon('quote'), + _centerToolbarIcon('number_list'), + _centerToolbarIcon('bulleted_list'), + ], + ), + ), + ); + } + + Widget _listToolbar(BuildContext context) { + return _centerToolbarIcon( + 'quote', + key: _listToolbarKey, + width: listToolbarWidth, + onTap: () => _onTapListToolbar(context), + ); + } + + Widget _centerToolbarIcon(String name, + {Key? key, double? width, VoidCallback? onTap}) { + return Tooltip( + key: key, + preferBelow: false, + message: name, + child: GestureDetector( + onTap: onTap ?? () => debugPrint('toolbar tap $name'), + child: SizedBox.fromSize( + size: width != null + ? Size(width, toolbarHeight) + : Size.square(toolbarHeight), + child: Center( + child: FlowySvg( + name: 'toolbar/$name', + ), + ), + ), + ), + ); + } + + void _onTapListToolbar(BuildContext context) { + // TODO: implement more detailed UI. + final items = defaultListToolbarEventHandlers + .map((handler) => handler.keys.first) + .toList(growable: false); + final renderBox = + _listToolbarKey.currentContext?.findRenderObject() as RenderBox; + final offset = renderBox + .localToGlobal(Offset.zero) + .translate(0, toolbarHeight - cornerRadius); + final rect = offset & Size(listToolbarWidth, listToolbarHeight); + + _listToolbarOverlay?.remove(); + _listToolbarOverlay = OverlayEntry(builder: (context) { + return Positioned.fromRect( + rect: rect, + child: Material( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(cornerRadius), + bottomRight: Radius.circular(cornerRadius), + ), + color: const Color(0xFF333333), + child: SingleChildScrollView( + child: ListView.builder( + itemExtent: toolbarHeight, + padding: const EdgeInsets.only(bottom: 10.0), + shrinkWrap: true, + itemCount: items.length, + itemBuilder: ((context, index) { + return ListTile( + contentPadding: const EdgeInsets.only( + left: 3.0, + right: 3.0, + ), + minVerticalPadding: 0.0, + title: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + items[index], + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + ), + ), + ), + onTap: () { + debugPrint('tap on $index'); + }, + ); + }), + ), + ), + ), + ); + }); + Overlay.of(context)?.insert(_listToolbarOverlay!); + } + + void _onSelectionChange() { + _listToolbarOverlay?.remove(); + _listToolbarOverlay = null; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 71143a3ede..6d21699625 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,4 +1,4 @@ -import 'package:flowy_editor/service/internal_key_event_handlers/delele_text_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; import 'package:flutter/material.dart'; @@ -10,7 +10,6 @@ import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/render/rich_text/number_list_text.dart'; import 'package:flowy_editor/render/rich_text/quoted_text.dart'; -import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; import 'package:flowy_editor/service/input_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; @@ -19,7 +18,7 @@ import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handle import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; -import 'package:flowy_editor/service/shortcut_service.dart'; +import 'package:flowy_editor/service/toolbar_service.dart'; NodeWidgetBuilders defaultBuilders = { 'editor': EditorEntryWidgetBuilder(), @@ -46,7 +45,6 @@ class FlowyEditor extends StatefulWidget { required this.editorState, this.customBuilders = const {}, this.keyEventHandlers = const [], - this.shortcuts = const [], }) : super(key: key); final EditorState editorState; @@ -57,9 +55,6 @@ class FlowyEditor extends StatefulWidget { /// Keyboard event handlers. final List keyEventHandlers; - /// Shortcuts - final FloatingShortcuts shortcuts; - @override State createState() => _FlowyEditorState(); } @@ -98,11 +93,9 @@ class _FlowyEditorState extends State { ...widget.keyEventHandlers, ], editorState: editorState, - child: FloatingShortcut( - key: editorState.service.floatingShortcutServiceKey, - size: const Size(200, 150), // TODO: support customize size. + child: FlowyToolbar( + key: editorState.service.toolbarServiceKey, editorState: editorState, - floatingShortcuts: widget.shortcuts, child: editorState.service.renderPluginService.buildPluginWidget( NodeWidgetContext( context: context, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 375ebee31c..f4034d4518 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -9,7 +9,7 @@ import 'package:flowy_editor/render/selection/selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flutter/gestures.dart'; -import 'package:flowy_editor/service/shortcut_service.dart'; +import 'package:flowy_editor/service/toolbar_service.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flutter/material.dart'; @@ -452,9 +452,7 @@ class _FlowySelectionState extends State ..forEach((overlay) => overlay.remove()) ..clear(); // clear floating shortcuts - editorState.service.floatingShortcutServiceKey.currentState - ?.unwrapOrNull() - ?.hide(); + editorState.service.toolbarService.hide(); } void _updateSelection(Selection selection) { @@ -464,6 +462,9 @@ class _FlowySelectionState extends State currentSelection = selection; currentSelectedNodes.value = nodes; + Rect? topmostRect; + LayerLink? layerLink; + var index = 0; for (final node in nodes) { final selectable = node.selectable; @@ -502,19 +503,28 @@ class _FlowySelectionState extends State final rects = selectable.getRectsInSelection(newSelection); for (final rect in rects) { + // FIXME: Need to compute more precise location. + topmostRect ??= rect; + layerLink ??= node.layerLink; + _rects.add(_transformRectToGlobal(selectable, rect)); final overlay = OverlayEntry( - builder: ((context) => SelectionWidget( - color: widget.selectionColor, - layerLink: node.layerLink, - rect: rect, - )), + builder: (context) => SelectionWidget( + color: widget.selectionColor, + layerLink: node.layerLink, + rect: rect, + ), ); _selectionOverlays.add(overlay); } index += 1; } Overlay.of(context)?.insertAll(_selectionOverlays); + + if (topmostRect != null && layerLink != null) { + editorState.service.toolbarService + .showInOffset(topmostRect.topLeft, layerLink); + } } Rect _transformRectToGlobal(Selectable selectable, Rect r) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 551002499d..93c103c40f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -1,5 +1,5 @@ import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/shortcut_service.dart'; +import 'package:flowy_editor/service/toolbar_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; import 'package:flutter/material.dart'; @@ -22,13 +22,11 @@ class FlowyService { late FlowyRenderPlugin renderPluginService; // floating shortcut service - final floatingShortcutServiceKey = + final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_floating_shortcut_service'); - FlowyFloatingShortcutService get floatingToolbarService { - assert(floatingShortcutServiceKey.currentState != null && - floatingShortcutServiceKey.currentState - is FlowyFloatingShortcutService); - return floatingShortcutServiceKey.currentState! - as FlowyFloatingShortcutService; + ToolbarService get toolbarService { + assert(toolbarServiceKey.currentState != null && + toolbarServiceKey.currentState is ToolbarService); + return toolbarServiceKey.currentState! as ToolbarService; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart deleted file mode 100644 index 774d906acc..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; -import 'package:flutter/material.dart'; - -mixin FlowyFloatingShortcutService { - /// Show the floating shortcut widget beside the offset. - void showInOffset(Offset offset, LayerLink layerLink); - - /// Hide the floating shortcut widget. - void hide(); -} - -class FloatingShortcut extends StatefulWidget { - const FloatingShortcut({ - Key? key, - required this.size, - required this.editorState, - required this.floatingShortcuts, - required this.child, - }) : super(key: key); - - final Size size; - final EditorState editorState; - final Widget child; - final FloatingShortcuts floatingShortcuts; - - @override - State createState() => _FloatingShortcutState(); -} - -class _FloatingShortcutState extends State - with FlowyFloatingShortcutService { - OverlayEntry? _floatintShortcutOverlay; - - @override - void showInOffset(Offset offset, LayerLink layerLink) { - _floatintShortcutOverlay?.remove(); - _floatintShortcutOverlay = OverlayEntry( - builder: (context) => FloatingShortcutWidget( - editorState: widget.editorState, - layerLink: layerLink, - rect: offset.translate(10, 0) & widget.size, - floatingShortcuts: widget.floatingShortcuts), - ); - Overlay.of(context)?.insert(_floatintShortcutOverlay!); - } - - @override - void hide() { - _floatintShortcutOverlay?.remove(); - _floatintShortcutOverlay = null; - } - - @override - Widget build(BuildContext context) { - return Container( - child: widget.child, - ); - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart new file mode 100644 index 0000000000..794e4da72b --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart @@ -0,0 +1,56 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/render/selection/toolbar_widget.dart'; +import 'package:flutter/material.dart'; + +mixin ToolbarService { + /// Show the floating shortcut widget beside the offset. + void showInOffset(Offset offset, LayerLink layerLink); + + /// Hide the floating shortcut widget. + void hide(); +} + +class FlowyToolbar extends StatefulWidget { + const FlowyToolbar({ + Key? key, + required this.editorState, + required this.child, + }) : super(key: key); + + final EditorState editorState; + final Widget child; + + @override + State createState() => _FlowyToolbarState(); +} + +class _FlowyToolbarState extends State with ToolbarService { + OverlayEntry? _floatingShortcutOverlay; + + @override + void showInOffset(Offset offset, LayerLink layerLink) { + _floatingShortcutOverlay?.remove(); + _floatingShortcutOverlay = OverlayEntry( + builder: (context) => ToolbarWidget( + editorState: widget.editorState, + layerLink: layerLink, + offset: offset.translate(0, -37.0), + handlers: const [], + ), + ); + Overlay.of(context)?.insert(_floatingShortcutOverlay!); + } + + @override + void hide() { + _floatingShortcutOverlay?.remove(); + _floatingShortcutOverlay = null; + } + + @override + Widget build(BuildContext context) { + return Container( + child: widget.child, + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 403ee2dddf..db0eef5296 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -26,7 +26,7 @@ dev_dependencies: flutter: # To add assets to your package, add an assets section, like this: assets: - - assets/images/uncheck.svg + - assets/images/toolbar/ - assets/images/ - assets/document.json # - images/a_dot_burr.jpeg From 846a273de8ec4e3f76c56ff880f4e7b53329f520 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 1 Aug 2022 17:48:16 +0800 Subject: [PATCH 106/121] feat: rename toolbar --- .../lib/service/selection_service.dart | 2 +- .../flowy_editor/lib/service/service.dart | 5 ++--- .../lib/service/toolbar_service.dart | 16 ++++++++-------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index f4034d4518..a6cee66829 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -451,7 +451,7 @@ class _FlowySelectionState extends State _cursorOverlays ..forEach((overlay) => overlay.remove()) ..clear(); - // clear floating shortcuts + // clear toolbar editorState.service.toolbarService.hide(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 93c103c40f..829ad2bde1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -21,9 +21,8 @@ class FlowyService { // render plugin service late FlowyRenderPlugin renderPluginService; - // floating shortcut service - final toolbarServiceKey = - GlobalKey(debugLabel: 'flowy_floating_shortcut_service'); + // toolbar service + final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service'); ToolbarService get toolbarService { assert(toolbarServiceKey.currentState != null && toolbarServiceKey.currentState is ToolbarService); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart index 794e4da72b..b8b8f95e46 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart @@ -3,10 +3,10 @@ import 'package:flowy_editor/render/selection/toolbar_widget.dart'; import 'package:flutter/material.dart'; mixin ToolbarService { - /// Show the floating shortcut widget beside the offset. + /// Show the toolbar widget beside the offset. void showInOffset(Offset offset, LayerLink layerLink); - /// Hide the floating shortcut widget. + /// Hide the toolbar widget. void hide(); } @@ -25,12 +25,12 @@ class FlowyToolbar extends StatefulWidget { } class _FlowyToolbarState extends State with ToolbarService { - OverlayEntry? _floatingShortcutOverlay; + OverlayEntry? _toolbarOverlay; @override void showInOffset(Offset offset, LayerLink layerLink) { - _floatingShortcutOverlay?.remove(); - _floatingShortcutOverlay = OverlayEntry( + _toolbarOverlay?.remove(); + _toolbarOverlay = OverlayEntry( builder: (context) => ToolbarWidget( editorState: widget.editorState, layerLink: layerLink, @@ -38,13 +38,13 @@ class _FlowyToolbarState extends State with ToolbarService { handlers: const [], ), ); - Overlay.of(context)?.insert(_floatingShortcutOverlay!); + Overlay.of(context)?.insert(_toolbarOverlay!); } @override void hide() { - _floatingShortcutOverlay?.remove(); - _floatingShortcutOverlay = null; + _toolbarOverlay?.remove(); + _toolbarOverlay = null; } @override From 06cab949f2a910b4d26ddbe40d94bd3ea0b70d28 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 1 Aug 2022 18:05:19 +0800 Subject: [PATCH 107/121] chore: delete unused import, and sort the imports. --- .../lib/service/selection_service.dart | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index a6cee66829..be1fd0bc8a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,18 +1,16 @@ import 'dart:async'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/render/selection/cursor_widget.dart'; -import 'package:flowy_editor/render/selection/selection_widget.dart'; -import 'package:flowy_editor/extensions/object_extensions.dart'; -import 'package:flowy_editor/extensions/node_extensions.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flowy_editor/service/toolbar_service.dart'; import 'package:flowy_editor/editor_state.dart'; - -import 'package:flutter/material.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flowy_editor/render/selection/cursor_widget.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/render/selection/selection_widget.dart'; /// Process selection and cursor mixin FlowySelectionService on State { From 2f58c54b81e01e302f6baea218cba549f91a6b87 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 1 Aug 2022 17:36:31 +0800 Subject: [PATCH 108/121] refactor: batch insert and delete nodes --- .../flowy_editor/lib/document/state_tree.dart | 22 +++++++++---- .../flowy_editor/lib/editor_state.dart | 4 +-- .../flowy_editor/lib/operation/operation.dart | 32 ++++++++++--------- .../lib/operation/transaction_builder.dart | 24 ++++++++++++-- .../flowy_editor/test/flowy_editor_test.dart | 7 ++-- .../flowy_editor/test/operation_test.dart | 12 +++---- 6 files changed, 63 insertions(+), 38 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 22f4b88c24..cf49f48ac8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -22,17 +22,21 @@ class StateTree { return root.childAtPath(path); } - bool insert(Path path, Node node) { + bool insert(Path path, List nodes) { if (path.isEmpty) { return false; } - final insertedNode = root.childAtPath( + Node? insertedNode = root.childAtPath( path.sublist(0, path.length - 1) + [path.last - 1], ); if (insertedNode == null) { return false; } - insertedNode.insertAfter(node); + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + insertedNode!.insertAfter(node); + insertedNode = node; + } return true; } @@ -48,13 +52,17 @@ class StateTree { return false; } - Node? delete(Path path) { + delete(Path path, [int length = 1]) { if (path.isEmpty) { return null; } - final deletedNode = root.childAtPath(path); - deletedNode?.unlink(); - return deletedNode; + var deletedNode = root.childAtPath(path); + while (deletedNode != null && length > 0) { + final next = deletedNode.next; + deletedNode.unlink(); + length--; + deletedNode = next; + } } Attributes? update(Path path, Attributes attributes) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 1019ad3510..277b742604 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -94,11 +94,11 @@ class EditorState { _applyOperation(Operation op) { if (op is InsertOperation) { - document.insert(op.path, op.value); + document.insert(op.path, op.nodes); } else if (op is UpdateOperation) { document.update(op.path, op.attributes); } else if (op is DeleteOperation) { - document.delete(op.path); + document.delete(op.path, op.nodes.length); } else if (op is TextEditOperation) { document.textEdit(op.path, op.delta); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index a505df4b1d..e07c196768 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -24,18 +24,19 @@ abstract class Operation { } class InsertOperation extends Operation { - final Node value; + final List nodes; factory InsertOperation.fromJson(Map map) { final path = map["path"] as List; - final value = Node.fromJson(map["value"]); + final value = + (map["nodes"] as List).map((n) => Node.fromJson(n)).toList(); return InsertOperation(path, value); } - InsertOperation(Path path, this.value) : super(path); + InsertOperation(Path path, this.nodes) : super(path); - InsertOperation copyWith({Path? path, Node? value}) => - InsertOperation(path ?? this.path, value ?? this.value); + InsertOperation copyWith({Path? path, List? nodes}) => + InsertOperation(path ?? this.path, nodes ?? this.nodes); @override Operation copyWithPath(Path path) => copyWith(path: path); @@ -44,7 +45,7 @@ class InsertOperation extends Operation { Operation invert() { return DeleteOperation( path, - value, + nodes, ); } @@ -53,7 +54,7 @@ class InsertOperation extends Operation { return { "type": "insert-operation", "path": path.toList(), - "value": value.toJson(), + "nodes": nodes.map((n) => n.toJson()), }; } } @@ -104,28 +105,29 @@ class UpdateOperation extends Operation { } class DeleteOperation extends Operation { - final Node removedValue; + final List nodes; factory DeleteOperation.fromJson(Map map) { final path = map["path"] as List; - final removedValue = Node.fromJson(map["removedValue"]); - return DeleteOperation(path, removedValue); + final List nodes = + (map["nodes"] as List).map((e) => Node.fromJson(e)).toList(); + return DeleteOperation(path, nodes); } DeleteOperation( Path path, - this.removedValue, + this.nodes, ) : super(path); - DeleteOperation copyWith({Path? path, Node? removedValue}) => - DeleteOperation(path ?? this.path, removedValue ?? this.removedValue); + DeleteOperation copyWith({Path? path, List? nodes}) => + DeleteOperation(path ?? this.path, nodes ?? this.nodes); @override Operation copyWithPath(Path path) => copyWith(path: path); @override Operation invert() { - return InsertOperation(path, removedValue); + return InsertOperation(path, nodes); } @override @@ -133,7 +135,7 @@ class DeleteOperation extends Operation { return { "type": "delete-operation", "path": path.toList(), - "removedValue": removedValue.toJson(), + "nodes": nodes.map((n) => n.toJson()), }; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 3b88ed1627..ac7fd4353f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -29,8 +29,12 @@ class TransactionBuilder { } insertNode(Path path, Node node) { + insertNodes(path, [node]); + } + + insertNodes(Path path, List nodes) { beforeSelection = state.cursorSelection; - add(InsertOperation(path, node)); + add(InsertOperation(path, nodes)); } updateNode(Node node, Attributes attributes) { @@ -43,14 +47,28 @@ class TransactionBuilder { } deleteNode(Node node) { - beforeSelection = state.cursorSelection; - add(DeleteOperation(node.path, node)); + deleteNodesAtPath(node.path); } deleteNodes(List nodes) { nodes.forEach(deleteNode); } + deleteNodesAtPath(Path path, [int length = 1]) { + if (path.isEmpty) { + return; + } + final nodes = []; + final prefix = path.sublist(0, path.length - 1); + final last = path.last; + for (var i = 0; i < length; i++) { + final node = state.document.nodeAtPath(prefix + [last + i])!; + nodes.add(node); + } + + add(DeleteOperation(path, nodes)); + } + textEdit(TextNode node, Delta Function() f) { beforeSelection = state.cursorSelection; final path = node.path; diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index 16ccadb079..49d0fd00f5 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:math'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/state_tree.dart'; @@ -50,7 +49,7 @@ void main() { final insertNode = Node.fromJson({ 'type': 'text', }); - bool result = stateTree.insert([1, 1], insertNode); + bool result = stateTree.insert([1, 1], [insertNode]); expect(result, true); expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true); }); @@ -59,9 +58,7 @@ void main() { final String response = await rootBundle.loadString('assets/document.json'); final data = Map.from(json.decode(response)); final stateTree = StateTree.fromJson(data); - final deletedNode = stateTree.delete([1, 1]); - expect(deletedNode != null, true); - expect(deletedNode!.attributes['text-type'], 'checkbox'); + stateTree.delete([1, 1], 1); final node = stateTree.nodeAtPath([1, 1]); expect(node != null, true); expect(node!.attributes['tag'], '**'); diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index c6811b86e2..7507cb65bf 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -28,17 +28,17 @@ void main() { test('insert + insert', () { final t = transformOperation( InsertOperation([0, 1], - Node(type: "node", attributes: {}, children: LinkedList())), + [Node(type: "node", attributes: {}, children: LinkedList())]), InsertOperation([0, 1], - Node(type: "node", attributes: {}, children: LinkedList()))); + [Node(type: "node", attributes: {}, children: LinkedList())])); expect(t.path, [0, 2]); }); test('delete + delete', () { final t = transformOperation( DeleteOperation([0, 1], - Node(type: "node", attributes: {}, children: LinkedList())), + [Node(type: "node", attributes: {}, children: LinkedList())]), DeleteOperation([0, 2], - Node(type: "node", attributes: {}, children: LinkedList()))); + [Node(type: "node", attributes: {}, children: LinkedList())])); expect(t.path, [0, 1]); }); }); @@ -85,7 +85,7 @@ void main() { { "type": "insert-operation", "path": [0], - "value": item1.toJson(), + "nodes": [item1.toJson()], } ], }); @@ -108,7 +108,7 @@ void main() { { "type": "delete-operation", "path": [0], - "removedValue": item1.toJson(), + "nodes": [item1.toJson()], } ], }); From 159fe63575be2521d87e1f636838564c087bd4a9 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 1 Aug 2022 22:41:30 +0800 Subject: [PATCH 109/121] feat: implement edit text style by command + x --- .../lib/extensions/text_node_extensions.dart | 88 +++++++++++++++++++ .../format_rich_text_style.dart | 86 ++++++++++++++++++ ...pdate_text_style_by_command_x_handler.dart | 72 +++------------ .../lib/service/selection_service.dart | 11 ++- 4 files changed, 194 insertions(+), 63 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart new file mode 100644 index 0000000000..29e90784ae --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart @@ -0,0 +1,88 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; + +extension TextNodeExtension on TextNode { + bool allSatisfyBoldInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.bold, selection); + + bool allSatisfyItalicInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.italic, selection); + + bool allSatisfyUnderlineInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.underline, selection); + + bool allSatisfyStrikethroughInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.strikethrough, selection); + + bool allSatisfyInSelection(String styleKey, Selection selection) { + final ops = delta.operations.whereType(); + var start = 0; + for (final op in ops) { + if (start >= selection.end.offset) { + break; + } + final length = op.length; + if (start < selection.end.offset && + start + length > selection.start.offset) { + if (op.attributes == null || + !op.attributes!.containsKey(styleKey) || + op.attributes![styleKey] == false) { + return false; + } + } + start += length; + } + return true; + } +} + +extension TextNodesExtension on List { + bool allSatisfyBoldInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.bold, selection); + + bool allSatisfyItalicInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.italic, selection); + + bool allSatisfyUnderlineInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.underline, selection); + + bool allSatisfyStrikethroughInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.strikethrough, selection); + + bool allSatisfyInSelection(String styleKey, Selection selection) { + if (isEmpty) { + return false; + } + if (length == 1) { + return first.allSatisfyInSelection(styleKey, selection); + } else { + for (var i = 0; i < length; i++) { + final node = this[i]; + final Selection newSelection; + if (i == 0 && pathEquals(node.path, selection.start.path)) { + newSelection = selection.copyWith( + end: Position(path: node.path, offset: node.toRawString().length), + ); + } else if (i == length - 1 && + pathEquals(node.path, selection.end.path)) { + newSelection = selection.copyWith( + start: Position(path: node.path, offset: 0), + ); + } else { + newSelection = Selection( + start: Position(path: node.path, offset: 0), + end: Position(path: node.path, offset: node.toRawString().length), + ); + } + if (!node.allSatisfyInSelection(styleKey, newSelection)) { + return false; + } + } + return true; + } + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart new file mode 100644 index 0000000000..c88eab833f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -0,0 +1,86 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; + +bool formatRichTextStyle( + EditorState editorState, Map attributes) { + final selection = editorState.service.selectionService.currentSelection; + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final textNodes = nodes.whereType().toList(); + + if (selection == null || textNodes.isEmpty) { + return false; + } + + final builder = TransactionBuilder(editorState); + + // 1. All nodes are text nodes. + // 2. The first node is not TextNode. + // 3. The last node is not TextNode. + if (textNodes.length == nodes.length) { + if (textNodes.length == 1) { + builder.formatText( + textNodes.first, + selection.start.offset, + selection.end.offset - selection.start.offset, + attributes, + ); + } else { + for (var i = 0; i < textNodes.length; i++) { + final node = textNodes[i]; + if (i == 0) { + builder.formatText( + node, + selection.start.offset, + node.toRawString().length - selection.start.offset, + attributes, + ); + } else if (i == textNodes.length - 1) { + builder.formatText( + node, + 0, + selection.end.offset, + attributes, + ); + } else { + builder.formatText( + node, + 0, + node.toRawString().length, + attributes, + ); + } + } + } + } else { + for (var i = 0; i < textNodes.length; i++) { + final node = textNodes[i]; + if (i == 0 && node == nodes.first) { + builder.formatText( + node, + selection.start.offset, + node.toRawString().length - selection.start.offset, + attributes, + ); + } else if (i == textNodes.length - 1 && node == nodes.last) { + builder.formatText( + node, + 0, + selection.end.offset, + attributes, + ); + } else { + builder.formatText( + node, + 0, + node.toRawString().length, + attributes, + ); + } + } + } + + builder.commit(); + + return true; +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 5f13484442..6e4b742785 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -1,22 +1,21 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/extensions/text_node_extensions.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; + FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { if (!event.isMetaPressed || event.character == null) { return KeyEventResult.ignored; } final selection = editorState.service.selectionService.currentSelection; - final nodes = editorState.service.selectionService.currentSelectedNodes.value - .whereType() - .toList(); + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final textNodes = nodes.whereType().toList(growable: false); - if (selection == null || nodes.isEmpty) { + if (selection == null || textNodes.isEmpty) { return KeyEventResult.ignored; } @@ -24,7 +23,9 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { // bold case 'B': case 'b': - _makeBold(editorState, nodes, selection); + formatRichTextStyle(editorState, { + StyleKey.bold: !textNodes.allSatisfyBoldInSelection(selection), + }); return KeyEventResult.handled; default: break; @@ -32,52 +33,3 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { return KeyEventResult.ignored; }; - -// TODO: implement unBold. -void _makeBold( - EditorState editorState, List nodes, Selection selection) { - final builder = TransactionBuilder(editorState); - if (nodes.length == 1) { - builder.formatText( - nodes.first, - selection.start.offset, - selection.end.offset - selection.start.offset, - { - 'bold': true, - }, - ); - } else { - for (var i = 0; i < nodes.length; i++) { - final node = nodes[i]; - if (i == 0) { - builder.formatText( - node, - selection.start.offset, - node.toRawString().length - selection.start.offset, - { - 'bold': true, - }, - ); - } else if (i == nodes.length - 1) { - builder.formatText( - node, - 0, - selection.end.offset, - { - 'bold': true, - }, - ); - } else { - builder.formatText( - node, - 0, - node.toRawString().length, - { - 'bold': true, - }, - ); - } - } - } - builder.commit(); -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index be1fd0bc8a..0695dd5e90 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -422,14 +422,19 @@ class _FlowySelectionState extends State // compute the selection in range. if (first != null && last != null) { - bool isDownward = panStartOffset!.dy <= panEndOffset!.dy; + bool isDownward; + if (first == last) { + isDownward = panStartOffset!.dx < panEndOffset!.dx; + } else { + isDownward = panStartOffset!.dy < panEndOffset!.dy; + } final start = first.getSelectionInRange(panStartOffset!, panEndOffset!).start; final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end; final selection = Selection( start: isDownward ? start : end, end: isDownward ? end : start); - debugPrint('[_onPanUpdate] $selection'); - editorState.updateCursorSelection(selection); + debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection'); + editorState.service.selectionService.updateSelection(selection); } } From 5ecfc4ff2e825e96a2a84025981b11b4fb69c6ac Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 09:45:19 +0800 Subject: [PATCH 110/121] feat: refactor the text editing code --- .../lib/operation/transaction_builder.dart | 3 +- .../format_rich_text_style.dart | 73 +++++-------------- 2 files changed, 20 insertions(+), 56 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index e70dfc411a..94b829519e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -53,7 +53,7 @@ class TransactionBuilder { } textEdit(TextNode node, Delta Function() f) { - beforeSelection = state.cursorSelection; + beforeSelection = state.service.selectionService.currentSelection; final path = node.path; final delta = f(); @@ -91,6 +91,7 @@ class TransactionBuilder { formatText(TextNode node, int index, int length, Attributes attributes) { textEdit(node, () => Delta().retain(index).retain(length, attributes)); + afterSelection = beforeSelection; } deleteText(TextNode node, int index, int length) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart index c88eab833f..5622785d45 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -17,66 +17,29 @@ bool formatRichTextStyle( // 1. All nodes are text nodes. // 2. The first node is not TextNode. // 3. The last node is not TextNode. - if (textNodes.length == nodes.length) { - if (textNodes.length == 1) { + for (var i = 0; i < textNodes.length; i++) { + final textNode = textNodes[i]; + if (i == 0 && textNode == nodes.first) { builder.formatText( - textNodes.first, + textNode, selection.start.offset, - selection.end.offset - selection.start.offset, + textNode.toRawString().length - selection.start.offset, + attributes, + ); + } else if (i == textNodes.length - 1 && textNode == nodes.last) { + builder.formatText( + textNode, + 0, + selection.end.offset, attributes, ); } else { - for (var i = 0; i < textNodes.length; i++) { - final node = textNodes[i]; - if (i == 0) { - builder.formatText( - node, - selection.start.offset, - node.toRawString().length - selection.start.offset, - attributes, - ); - } else if (i == textNodes.length - 1) { - builder.formatText( - node, - 0, - selection.end.offset, - attributes, - ); - } else { - builder.formatText( - node, - 0, - node.toRawString().length, - attributes, - ); - } - } - } - } else { - for (var i = 0; i < textNodes.length; i++) { - final node = textNodes[i]; - if (i == 0 && node == nodes.first) { - builder.formatText( - node, - selection.start.offset, - node.toRawString().length - selection.start.offset, - attributes, - ); - } else if (i == textNodes.length - 1 && node == nodes.last) { - builder.formatText( - node, - 0, - selection.end.offset, - attributes, - ); - } else { - builder.formatText( - node, - 0, - node.toRawString().length, - attributes, - ); - } + builder.formatText( + textNode, + 0, + textNode.toRawString().length, + attributes, + ); } } From ba78f0073dd4e6e6d8991a52b4425bc67e525d32 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 10:25:18 +0800 Subject: [PATCH 111/121] feat: implement bold text in toolbar service --- .../flowy_editor/lib/document/node.dart | 2 +- .../lib/render/selection/toolbar_widget.dart | 70 +++++++++---------- .../format_rich_text_style.dart | 55 +++++++++------ .../lib/service/toolbar_service.dart | 2 +- 4 files changed, 68 insertions(+), 61 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index bdd6da444d..69852856b0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -178,7 +178,7 @@ class TextNode extends Node { }) : _delta = delta; TextNode.empty() - : _delta = Delta([TextInsert('')]), + : _delta = Delta([TextInsert(' ')]), super( type: 'text', children: LinkedList(), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart index 7266929962..486bd97671 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart @@ -1,43 +1,35 @@ import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; import 'package:flutter/material.dart'; -typedef ToolbarEventHandler = void Function( - EditorState editorState, String eventName); +typedef ToolbarEventHandler = void Function(EditorState editorState); -typedef ToolbarEventHandlers = List>; -ToolbarEventHandlers defaultToolbarEventHandlers = [ - { - 'bold': ((editorState, eventName) {}), - 'italic': ((editorState, eventName) {}), - 'strikethrough': ((editorState, eventName) {}), - 'underline': ((editorState, eventName) {}), - 'quote': ((editorState, eventName) {}), - 'number_list': ((editorState, eventName) {}), - 'bulleted_list': ((editorState, eventName) {}), - } -]; +typedef ToolbarEventHandlers = Map; -ToolbarEventHandlers defaultListToolbarEventHandlers = [ - { - 'h1': ((editorState, eventName) {}), - }, - { - 'h2': ((editorState, eventName) {}), - }, - { - 'h3': ((editorState, eventName) {}), - }, - { - 'bulleted_list': ((editorState, eventName) {}), - }, - { - 'quote': ((editorState, eventName) {}), - } +ToolbarEventHandlers defaultToolbarEventHandlers = { + 'bold': ((editorState) { + formatRichTextStyle(editorState, {StyleKey.bold: true}); + }), + 'italic': ((editorState) {}), + 'strikethrough': ((editorState) {}), + 'underline': ((editorState) {}), + 'quote': ((editorState) {}), + 'number_list': ((editorState) {}), + 'bulleted_list': ((editorState) {}), +}; + +List defaultListToolbarEventNames = [ + 'H1', + 'H2', + 'H3', + 'B-List', + 'N-List', ]; class ToolbarWidget extends StatefulWidget { - ToolbarWidget({ + const ToolbarWidget({ Key? key, required this.editorState, required this.layerLink, @@ -137,7 +129,7 @@ class _ToolbarWidgetState extends State { preferBelow: false, message: name, child: GestureDetector( - onTap: onTap ?? () => debugPrint('toolbar tap $name'), + onTap: onTap ?? () => _onTap(name), child: SizedBox.fromSize( size: width != null ? Size(width, toolbarHeight) @@ -154,9 +146,7 @@ class _ToolbarWidgetState extends State { void _onTapListToolbar(BuildContext context) { // TODO: implement more detailed UI. - final items = defaultListToolbarEventHandlers - .map((handler) => handler.keys.first) - .toList(growable: false); + final items = defaultListToolbarEventNames; final renderBox = _listToolbarKey.currentContext?.findRenderObject() as RenderBox; final offset = renderBox @@ -198,7 +188,7 @@ class _ToolbarWidgetState extends State { ), ), onTap: () { - debugPrint('tap on $index'); + _onTap(items[index]); }, ); }), @@ -210,6 +200,14 @@ class _ToolbarWidgetState extends State { Overlay.of(context)?.insert(_listToolbarOverlay!); } + void _onTap(String eventName) { + if (defaultToolbarEventHandlers.containsKey(eventName)) { + defaultToolbarEventHandlers[eventName]!(widget.editorState); + return; + } + assert(false, 'Could not find the event handler for $eventName'); + } + void _onSelectionChange() { _listToolbarOverlay?.remove(); _listToolbarOverlay = null; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart index 5622785d45..162818ae1d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -17,29 +17,38 @@ bool formatRichTextStyle( // 1. All nodes are text nodes. // 2. The first node is not TextNode. // 3. The last node is not TextNode. - for (var i = 0; i < textNodes.length; i++) { - final textNode = textNodes[i]; - if (i == 0 && textNode == nodes.first) { - builder.formatText( - textNode, - selection.start.offset, - textNode.toRawString().length - selection.start.offset, - attributes, - ); - } else if (i == textNodes.length - 1 && textNode == nodes.last) { - builder.formatText( - textNode, - 0, - selection.end.offset, - attributes, - ); - } else { - builder.formatText( - textNode, - 0, - textNode.toRawString().length, - attributes, - ); + if (nodes.length == textNodes.length && textNodes.length == 1) { + builder.formatText( + textNodes.first, + selection.start.offset, + selection.end.offset - selection.start.offset, + attributes, + ); + } else { + for (var i = 0; i < textNodes.length; i++) { + final textNode = textNodes[i]; + if (i == 0 && textNode == nodes.first) { + builder.formatText( + textNode, + selection.start.offset, + textNode.toRawString().length - selection.start.offset, + attributes, + ); + } else if (i == textNodes.length - 1 && textNode == nodes.last) { + builder.formatText( + textNode, + 0, + selection.end.offset, + attributes, + ); + } else { + builder.formatText( + textNode, + 0, + textNode.toRawString().length, + attributes, + ); + } } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart index b8b8f95e46..feb293aad4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart @@ -35,7 +35,7 @@ class _FlowyToolbarState extends State with ToolbarService { editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), - handlers: const [], + handlers: const {}, ), ); Overlay.of(context)?.insert(_toolbarOverlay!); From b11a127432cb67a3708c2d194557efd4d557d8e5 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 11:01:58 +0800 Subject: [PATCH 112/121] feat: implement italic, strikethrough and underline in toolbar service --- .../flowy_editor/lib/document/attributes.dart | 2 +- .../flowy_editor/lib/document/node.dart | 6 ++- .../lib/render/selection/toolbar_widget.dart | 10 ++--- .../format_rich_text_style.dart | 44 ++++++++++++++++++- ...pdate_text_style_by_command_x_handler.dart | 4 +- 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart index 6e845420ef..4e1f39775f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart @@ -26,7 +26,7 @@ Attributes? composeAttributes(Attributes? a, Attributes? b) { a ??= {}; b ??= {}; final Attributes attributes = {}; - attributes.addAll(b); + attributes.addAll(Map.from(b)..removeWhere((_, value) => value == null)); for (final entry in a.entries) { if (!b.containsKey(entry.key)) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 69852856b0..0404be2a26 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -89,7 +89,11 @@ class Node extends ChangeNotifier with LinkedListEntry { this.attributes['subtype'] != attributes['subtype']; for (final attribute in attributes.entries) { - this.attributes[attribute.key] = attribute.value; + if (attribute.value == null) { + this.attributes.remove(attribute.key); + } else { + this.attributes[attribute.key] = attribute.value; + } } // Notify the new attributes // if attributes contains 'subtype', should notify parent to rebuild node diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart index 486bd97671..1a05b6ef10 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart @@ -9,12 +9,10 @@ typedef ToolbarEventHandler = void Function(EditorState editorState); typedef ToolbarEventHandlers = Map; ToolbarEventHandlers defaultToolbarEventHandlers = { - 'bold': ((editorState) { - formatRichTextStyle(editorState, {StyleKey.bold: true}); - }), - 'italic': ((editorState) {}), - 'strikethrough': ((editorState) {}), - 'underline': ((editorState) {}), + 'bold': (editorState) => formatBold(editorState), + 'italic': (editorState) => formatItalic(editorState), + 'strikethrough': (editorState) => formatStrikethrough(editorState), + 'underline': (editorState) => formatUnderline(editorState), 'quote': ((editorState) {}), 'number_list': ((editorState) {}), 'bulleted_list': ((editorState) {}), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart index 162818ae1d..514a9d706b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -1,9 +1,49 @@ +import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/extensions/text_node_extensions.dart'; -bool formatRichTextStyle( - EditorState editorState, Map attributes) { +bool formatBold(EditorState editorState) { + return formatRichText(editorState, StyleKey.bold); +} + +bool formatItalic(EditorState editorState) { + return formatRichText(editorState, StyleKey.italic); +} + +bool formatUnderline(EditorState editorState) { + return formatRichText(editorState, StyleKey.underline); +} + +bool formatStrikethrough(EditorState editorState) { + return formatRichText(editorState, StyleKey.strikethrough); +} + +bool formatRichText(EditorState editorState, String styleKey) { + final selection = editorState.service.selectionService.currentSelection; + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final textNodes = nodes.whereType().toList(growable: false); + + if (selection == null || textNodes.isEmpty) { + return false; + } + + bool value = !textNodes.allSatisfyInSelection(styleKey, selection); + Attributes attributes = { + styleKey: value, + }; + if (styleKey == StyleKey.underline && value) { + attributes[StyleKey.strikethrough] = null; + } else if (styleKey == StyleKey.strikethrough && value) { + attributes[StyleKey.underline] = null; + } + + return formatRichTextStyle(editorState, attributes); +} + +bool formatRichTextStyle(EditorState editorState, Attributes attributes) { final selection = editorState.service.selectionService.currentSelection; final nodes = editorState.service.selectionService.currentSelectedNodes.value; final textNodes = nodes.whereType().toList(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 6e4b742785..6b1fbcd9ca 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -23,9 +23,7 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { // bold case 'B': case 'b': - formatRichTextStyle(editorState, { - StyleKey.bold: !textNodes.allSatisfyBoldInSelection(selection), - }); + formatBold(editorState); return KeyEventResult.handled; default: break; From 9b6afcc5c91770b4fd7e790adbc42882fbdd8d28 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 11:42:16 +0800 Subject: [PATCH 113/121] feat: implement heading, quote, bulleted_list in toolbar service --- .../lib/render/rich_text/checkbox_text.dart | 2 +- .../lib/render/rich_text/rich_text_style.dart | 66 +++++++++++-------- .../lib/render/selection/toolbar_widget.dart | 18 +++-- .../format_rich_text_style.dart | 62 +++++++++++++++-- 4 files changed, 105 insertions(+), 43 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index 4d52e41867..ba2c5b8712 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -21,7 +21,7 @@ class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder { @override NodeValidator get nodeValidator => ((node) { - return node.attributes.containsKey(StyleKey.check); + return node.attributes.containsKey(StyleKey.checkbox); }); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index 26d4275774..19aa109faf 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -25,26 +25,48 @@ class StyleKey { static String font = 'font'; static String href = 'href'; - static String quote = 'quote'; - static String list = 'list'; - static String number = 'number'; - static String todo = 'todo'; - static String code = 'code'; - static String subtype = 'subtype'; - static String check = 'checkbox'; static String heading = 'heading'; + static String h1 = 'h1'; + static String h2 = 'h2'; + static String h3 = 'h3'; + static String h4 = 'h4'; + static String h5 = 'h5'; + static String h6 = 'h6'; + + static String bulletedList = 'bulleted-list'; + static String numberList = 'number-list'; + + static String quote = 'quote'; + static String checkbox = 'checkbox'; + static String code = 'code'; + static String number = 'number'; + + static List partialStyleKeys = [ + StyleKey.bold, + StyleKey.italic, + StyleKey.underline, + StyleKey.strikethrough, + ]; + + static List globalStyleKeys = [ + StyleKey.heading, + StyleKey.bulletedList, + StyleKey.numberList, + StyleKey.quote, + StyleKey.code, + ]; } double baseFontSize = 16.0; // TODO: customize. Map headingToFontSize = { - 'h1': baseFontSize + 15, - 'h2': baseFontSize + 12, - 'h3': baseFontSize + 9, - 'h4': baseFontSize + 6, - 'h5': baseFontSize + 3, - 'h6': baseFontSize, + StyleKey.h1: baseFontSize + 15, + StyleKey.h2: baseFontSize + 12, + StyleKey.h3: baseFontSize + 9, + StyleKey.h4: baseFontSize + 6, + StyleKey.h5: baseFontSize + 3, + StyleKey.h6: baseFontSize, }; extension NodeAttributesExtensions on Attributes { @@ -73,13 +95,6 @@ extension NodeAttributesExtensions on Attributes { return null; } - String? get list { - if (containsKey(StyleKey.list) && this[StyleKey.list] is String) { - return this[StyleKey.list]; - } - return null; - } - int? get number { if (containsKey(StyleKey.number) && this[StyleKey.number] is int) { return this[StyleKey.number]; @@ -87,13 +102,6 @@ extension NodeAttributesExtensions on Attributes { return null; } - bool get todo { - if (containsKey(StyleKey.todo) && this[StyleKey.todo] is bool) { - return this[StyleKey.todo]; - } - return false; - } - bool get code { if (containsKey(StyleKey.code) && this[StyleKey.code] == true) { return this[StyleKey.code]; @@ -102,8 +110,8 @@ extension NodeAttributesExtensions on Attributes { } bool get check { - if (containsKey(StyleKey.check) && this[StyleKey.check] is bool) { - return this[StyleKey.check]; + if (containsKey(StyleKey.checkbox) && this[StyleKey.checkbox] is bool) { + return this[StyleKey.checkbox]; } return false; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart index 1a05b6ef10..c06776a0a3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart @@ -1,8 +1,9 @@ +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flutter/material.dart'; + import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; -import 'package:flutter/material.dart'; typedef ToolbarEventHandler = void Function(EditorState editorState); @@ -13,17 +14,20 @@ ToolbarEventHandlers defaultToolbarEventHandlers = { 'italic': (editorState) => formatItalic(editorState), 'strikethrough': (editorState) => formatStrikethrough(editorState), 'underline': (editorState) => formatUnderline(editorState), - 'quote': ((editorState) {}), - 'number_list': ((editorState) {}), - 'bulleted_list': ((editorState) {}), + 'quote': (editorState) => formatQuote(editorState), + 'number_list': (editorState) {}, + 'bulleted_list': (editorState) => formatBulletedList(editorState), + 'H1': (editorState) => formatHeading(editorState, StyleKey.h1), + 'H2': (editorState) => formatHeading(editorState, StyleKey.h2), + 'H3': (editorState) => formatHeading(editorState, StyleKey.h3), }; List defaultListToolbarEventNames = [ 'H1', 'H2', 'H3', - 'B-List', - 'N-List', + // 'B-List', + // 'N-List', ]; class ToolbarWidget extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart index 514a9d706b..73945e5d88 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -1,27 +1,77 @@ import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/extensions/text_node_extensions.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/extensions/text_node_extensions.dart'; + +void formatHeading(EditorState editorState, String heading) { + formatTextNodes(editorState, { + StyleKey.subtype: StyleKey.heading, + StyleKey.heading: heading, + }); +} + +void formatQuote(EditorState editorState) { + formatTextNodes(editorState, { + StyleKey.subtype: StyleKey.quote, + }); +} + +void formatCheckbox(EditorState editorState) { + formatTextNodes(editorState, { + StyleKey.subtype: StyleKey.checkbox, + StyleKey.checkbox: false, + }); +} + +void formatBulletedList(EditorState editorState) { + formatTextNodes(editorState, { + StyleKey.subtype: StyleKey.bulletedList, + }); +} + +bool formatTextNodes(EditorState editorState, Attributes attributes) { + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final textNodes = nodes.whereType().toList(); + + if (textNodes.isEmpty) { + return false; + } + + final builder = TransactionBuilder(editorState); + + for (final textNode in textNodes) { + builder.updateNode( + textNode, + Attributes.fromIterable( + StyleKey.globalStyleKeys, + value: (_) => null, + )..addAll(attributes), + ); + } + + builder.commit(); + return true; +} bool formatBold(EditorState editorState) { - return formatRichText(editorState, StyleKey.bold); + return formatRichTextPartialStyle(editorState, StyleKey.bold); } bool formatItalic(EditorState editorState) { - return formatRichText(editorState, StyleKey.italic); + return formatRichTextPartialStyle(editorState, StyleKey.italic); } bool formatUnderline(EditorState editorState) { - return formatRichText(editorState, StyleKey.underline); + return formatRichTextPartialStyle(editorState, StyleKey.underline); } bool formatStrikethrough(EditorState editorState) { - return formatRichText(editorState, StyleKey.strikethrough); + return formatRichTextPartialStyle(editorState, StyleKey.strikethrough); } -bool formatRichText(EditorState editorState, String styleKey) { +bool formatRichTextPartialStyle(EditorState editorState, String styleKey) { final selection = editorState.service.selectionService.currentSelection; final nodes = editorState.service.selectionService.currentSelectedNodes.value; final textNodes = nodes.whereType().toList(growable: false); From 1ba15b321b0cf1454ab8e8771e04d9b9c37c0b7b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 11:47:48 +0800 Subject: [PATCH 114/121] feat: implement revert text in toolbar service --- .../flowy_editor/lib/render/selection/toolbar_widget.dart | 2 ++ .../default_text_operations/format_rich_text_style.dart | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart index c06776a0a3..91659e1d1f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart @@ -17,12 +17,14 @@ ToolbarEventHandlers defaultToolbarEventHandlers = { 'quote': (editorState) => formatQuote(editorState), 'number_list': (editorState) {}, 'bulleted_list': (editorState) => formatBulletedList(editorState), + 'Text': (editorState) => formatText(editorState), 'H1': (editorState) => formatHeading(editorState, StyleKey.h1), 'H2': (editorState) => formatHeading(editorState, StyleKey.h2), 'H3': (editorState) => formatHeading(editorState, StyleKey.h3), }; List defaultListToolbarEventNames = [ + 'Text', 'H1', 'H2', 'H3', diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart index 73945e5d88..79e7bfe077 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -5,6 +5,10 @@ import 'package:flowy_editor/extensions/text_node_extensions.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +void formatText(EditorState editorState) { + formatTextNodes(editorState, {}); +} + void formatHeading(EditorState editorState, String heading) { formatTextNodes(editorState, { StyleKey.subtype: StyleKey.heading, From 14bd18e21c5d9fbd78bdb96c97c1db16d5e4d793 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 14:27:16 +0800 Subject: [PATCH 115/121] feat: implement enter key event handler and keep attributes after insert --- .../flowy_editor/lib/document/node.dart | 13 +++++++ .../lib/operation/transaction_builder.dart | 21 +++++++----- .../enter_in_edge_of_text_node_handler.dart | 34 +++++++++++++------ ...pdate_text_style_by_command_x_handler.dart | 2 -- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 0404be2a26..3a7ad36456 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -205,6 +205,19 @@ class TextNode extends Node { return map; } + TextNode copyWith({ + String? type, + LinkedList? children, + Attributes? attributes, + Delta? delta, + }) => + TextNode( + type: type ?? this.type, + children: children ?? this.children, + attributes: attributes ?? this.attributes, + delta: delta ?? this.delta, + ); + // TODO: It's unneccesry to compute everytime. String toRawString() => _delta.operations.whereType().map((op) => op.content).join(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 94b829519e..9233a1b08a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -1,19 +1,18 @@ import 'dart:collection'; -import 'dart:math'; -import 'package:flowy_editor/editor_state.dart'; + +import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/text_delta.dart'; -import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/selection.dart'; - -import './operation.dart'; -import './transaction.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/operation/transaction.dart'; /// A [TransactionBuilder] is used to build the transaction from the state. /// It will save make a snapshot of the cursor selection state automatically. -/// The cursor can be resoted if the transaction is undo. +/// The cursor can be resorted if the transaction is undo. class TransactionBuilder { final List operations = []; @@ -30,8 +29,12 @@ class TransactionBuilder { } insertNode(Path path, Node node) { - beforeSelection = state.cursorSelection; + beforeSelection = state.service.selectionService.currentSelection; add(InsertOperation(path: path, value: node)); + // FIXME: Not exactly correct, needs to be customized. + afterSelection = Selection.collapsed( + Position(path: path, offset: 0), + ); } updateNode(Node node, Attributes attributes) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart index d1e89d393e..8b0c7c5423 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart @@ -1,13 +1,16 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/extensions/path_extensions.dart'; -import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'dart:collection'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flowy_editor/extensions/path_extensions.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; + FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.enter) { return KeyEventResult.ignored; @@ -23,12 +26,19 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { } final textNode = nodes.first as TextNode; - if (textNode.selectable!.end() == selection.end) { + final needCopyAttributes = StyleKey.globalStyleKeys + .where((key) => key != StyleKey.heading) + .contains(textNode.subtype); TransactionBuilder(editorState) ..insertNode( textNode.path.next, - TextNode.empty(), + textNode.copyWith( + children: LinkedList(), + delta: Delta([TextInsert(' ')]), + attributes: + needCopyAttributes ? {StyleKey.subtype: textNode.subtype} : null, + ), ) ..commit(); return KeyEventResult.handled; @@ -36,7 +46,11 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { TransactionBuilder(editorState) ..insertNode( textNode.path, - TextNode.empty(), + textNode.copyWith( + children: LinkedList(), + delta: Delta([TextInsert(' ')]), + attributes: {}, + ), ) ..commit(); return KeyEventResult.handled; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 6b1fbcd9ca..b062480cf2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/extensions/text_node_extensions.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; From 56ac86fb5192bca49438be3df580a03e28d726ae Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 15:32:43 +0800 Subject: [PATCH 116/121] chore: add some comments in image plugins. --- .../example/lib/plugin/image_node_widget.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 25d432b759..6a01fb6430 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,6 +1,23 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; +/// 1. define your custom type in example.json +/// For example I need to define an image plugin, then I define type equals +/// "image", and add "image_src" into "attributes". +/// { +/// "type": "image", +/// "attributes", { "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" } +/// } +/// 2. create a class extends [NodeWidgetBuilder] +/// 3. override the function `Widget build(NodeWidgetContext context)` +/// and return a widget to render. The returned widget should be +/// a StatefulWidget and mixin with [Selectable]. +/// +/// 4. override the getter `nodeValidator` +/// to verify the data structure in [Node]. +/// 5. register the plugin with `type` to `flowy_editor` in `main.dart`. +/// 6. Congratulations! + class ImageNodeBuilder extends NodeWidgetBuilder { @override Widget build(NodeWidgetContext context) { From fa05170c86029e28f74398a122df31314d44375a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 16:25:35 +0800 Subject: [PATCH 117/121] chore: fix test error --- .../lib/operation/transaction_builder.dart | 8 ++------ .../enter_in_edge_of_text_node_handler.dart | 14 ++++++++++++++ .../packages/flowy_editor/test/operation_test.dart | 3 ++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index dd531d5d5e..88e0c00890 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -30,14 +30,10 @@ class TransactionBuilder { insertNode(Path path, Node node) { insertNodes(path, [node]); - // FIXME: Not exactly correct, needs to be customized. - afterSelection = Selection.collapsed( - Position(path: path, offset: 0), - ); } insertNodes(Path path, List nodes) { - beforeSelection = state.service.selectionService.currentSelection; + beforeSelection = state.cursorSelection; add(InsertOperation(path, nodes)); } @@ -74,7 +70,7 @@ class TransactionBuilder { } textEdit(TextNode node, Delta Function() f) { - beforeSelection = state.service.selectionService.currentSelection; + beforeSelection = state.cursorSelection; final path = node.path; final delta = f(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart index b08dcb1f80..525afd9021 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flowy_editor/extensions/path_extensions.dart'; @@ -40,6 +42,12 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { needCopyAttributes ? {StyleKey.subtype: textNode.subtype} : {}, ), ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path.next, + offset: 0, + ), + ) ..commit(); return KeyEventResult.handled; } else if (textNode.selectable!.start() == selection.start) { @@ -52,6 +60,12 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { attributes: {}, ), ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path.next, + offset: 0, + ), + ) ..commit(); return KeyEventResult.handled; } diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index 7507cb65bf..339807cea4 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -8,6 +8,7 @@ import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/state_tree.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); group('transform path', () { test('transform path changed', () { expect(transformPath([0, 1], [0, 1]), [0, 2]); @@ -87,7 +88,7 @@ void main() { "path": [0], "nodes": [item1.toJson()], } - ], + ] }); }); test("delete", () { From 5fdcdbd357bc61cc06355e629d1fe36306668554 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 17:15:07 +0800 Subject: [PATCH 118/121] fix: bulleted-list typo --- .../packages/flowy_editor/example/assets/example.json | 6 +++--- .../packages/flowy_editor/lib/service/editor_service.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 9f1d278e16..c69237f24f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -144,7 +144,7 @@ } ], "attributes": { - "subtype": "bullet-list" + "subtype": "bulleted-list" } }, { @@ -155,7 +155,7 @@ } ], "attributes": { - "subtype": "bullet-list" + "subtype": "bulleted-list" } }, { @@ -170,7 +170,7 @@ } ], "attributes": { - "subtype": "bullet-list" + "subtype": "bulleted-list" } }, { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 6d21699625..b62fe1bb15 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -25,7 +25,7 @@ NodeWidgetBuilders defaultBuilders = { 'text': RichTextNodeWidgetBuilder(), 'text/checkbox': CheckboxNodeWidgetBuilder(), 'text/heading': HeadingTextNodeWidgetBuilder(), - 'text/bullet-list': BulletedListTextNodeWidgetBuilder(), + 'text/bulleted-list': BulletedListTextNodeWidgetBuilder(), 'text/number-list': NumberListTextNodeWidgetBuilder(), 'text/quote': QuotedTextNodeWidgetBuilder(), }; From a1be60721e9ad5655c0420ce4822a1c39f6a7e48 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 17:16:02 +0800 Subject: [PATCH 119/121] fix: pressing enter key in the edge of node doesn't work good. --- .../packages/flowy_editor/example/lib/main.dart | 17 ++++++++++------- .../lib/render/rich_text/flowy_rich_text.dart | 2 +- .../lib/render/rich_text/rich_text_style.dart | 1 + .../enter_in_edge_of_text_node_handler.dart | 7 +++---- .../lib/service/selection_service.dart | 7 +++++-- .../flowy_editor/lib/service/service.dart | 10 ++++++---- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 158e33bbb1..1a68f38ead 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -116,13 +116,16 @@ class _MyHomePageState extends State { _editorState = EditorState( document: document, ); - return FlowyEditor( - key: editorKey, - editorState: _editorState, - keyEventHandlers: const [], - customBuilders: { - 'image': ImageNodeBuilder(), - }, + return Container( + padding: const EdgeInsets.only(left: 20, right: 20), + child: FlowyEditor( + key: editorKey, + editorState: _editorState, + keyEventHandlers: const [], + customBuilders: { + 'image': ImageNodeBuilder(), + }, + ), // shortcuts: [ // // TODO: this won't work, just a example for now. // { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 70834184cc..f302fcaba8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -74,7 +74,7 @@ class _FlowyRichTextState extends State with Selectable { _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); final cursorHeight = widget.cursorHeight ?? _renderParagraph.getFullHeightForCaret(textPosition) ?? - 5.0; // default height + 18.0; // default height return Rect.fromLTWH( cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index 19aa109faf..cc4f6038ac 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -51,6 +51,7 @@ class StyleKey { static List globalStyleKeys = [ StyleKey.heading, + StyleKey.checkbox, StyleKey.bulletedList, StyleKey.numberList, StyleKey.quote, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart index 525afd9021..5b49907138 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart @@ -37,9 +37,8 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { textNode.path.next, textNode.copyWith( children: LinkedList(), - delta: Delta([TextInsert(' ')]), - attributes: - needCopyAttributes ? {StyleKey.subtype: textNode.subtype} : {}, + delta: Delta([TextInsert('')]), + attributes: needCopyAttributes ? textNode.attributes : {}, ), ) ..afterSelection = Selection.collapsed( @@ -56,7 +55,7 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { textNode.path, textNode.copyWith( children: LinkedList(), - delta: Delta([TextInsert(' ')]), + delta: Delta([TextInsert('')]), attributes: {}, ), ) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 0695dd5e90..59632773e5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -233,6 +233,9 @@ class _FlowySelectionState extends State @override void dispose() { + clearSelection(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); } @@ -455,7 +458,7 @@ class _FlowySelectionState extends State ..forEach((overlay) => overlay.remove()) ..clear(); // clear toolbar - editorState.service.toolbarService.hide(); + editorState.service.toolbarService?.hide(); } void _updateSelection(Selection selection) { @@ -526,7 +529,7 @@ class _FlowySelectionState extends State if (topmostRect != null && layerLink != null) { editorState.service.toolbarService - .showInOffset(topmostRect.topLeft, layerLink); + ?.showInOffset(topmostRect.topLeft, layerLink); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 829ad2bde1..937a16044a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -23,9 +23,11 @@ class FlowyService { // toolbar service final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service'); - ToolbarService get toolbarService { - assert(toolbarServiceKey.currentState != null && - toolbarServiceKey.currentState is ToolbarService); - return toolbarServiceKey.currentState! as ToolbarService; + ToolbarService? get toolbarService { + if (toolbarServiceKey.currentState != null && + toolbarServiceKey.currentState is ToolbarService) { + return toolbarServiceKey.currentState! as ToolbarService; + } + return null; } } From 397f43cbe132819af18ce9f0acfc7aa7576f9623 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 20:08:32 +0800 Subject: [PATCH 120/121] feat: implement remove subtype if text node is empty when pressing enter key --- .../enter_in_edge_of_text_node_handler.dart | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart index 5b49907138..ccdfcad5dc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart @@ -29,25 +29,46 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { final textNode = nodes.first as TextNode; if (textNode.selectable!.end() == selection.end) { - final needCopyAttributes = StyleKey.globalStyleKeys - .where((key) => key != StyleKey.heading) - .contains(textNode.subtype); - TransactionBuilder(editorState) - ..insertNode( - textNode.path.next, - textNode.copyWith( - children: LinkedList(), - delta: Delta([TextInsert('')]), - attributes: needCopyAttributes ? textNode.attributes : {}, - ), - ) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path.next, - offset: 0, - ), - ) - ..commit(); + if (textNode.subtype != null && textNode.delta.length == 0) { + TransactionBuilder(editorState) + ..deleteNode(textNode) + ..insertNode( + textNode.path, + textNode.copyWith( + children: LinkedList(), + delta: Delta([TextInsert('')]), + attributes: {}, + ), + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: 0, + ), + ) + ..commit(); + } else { + final needCopyAttributes = StyleKey.globalStyleKeys + .where((key) => key != StyleKey.heading) + .contains(textNode.subtype); + TransactionBuilder(editorState) + ..insertNode( + textNode.path.next, + textNode.copyWith( + children: LinkedList(), + delta: Delta([TextInsert('')]), + attributes: needCopyAttributes ? textNode.attributes : {}, + ), + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path.next, + offset: 0, + ), + ) + ..commit(); + } + return KeyEventResult.handled; } else if (textNode.selectable!.start() == selection.start) { TransactionBuilder(editorState) From c7432e640be8dcb56e5d0a9124e9a4f417223278 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Aug 2022 20:22:39 +0800 Subject: [PATCH 121/121] feat: delay rendering selection(need to be refactored). --- .../app_flowy/packages/flowy_editor/lib/editor_state.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 277b742604..92a05fc880 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -60,7 +60,12 @@ class EditorState { for (final op in transaction.operations) { _applyOperation(op); } - updateCursorSelection(transaction.afterSelection); + // updateCursorSelection(transaction.afterSelection); + + // FIXME: don't use delay + Future.delayed(const Duration(milliseconds: 16), () { + updateCursorSelection(transaction.afterSelection); + }); if (options.recordUndo) { final undoItem = undoManager.getUndoHistoryItem();