mirror of
https://github.com/AppFlowy-IO/AppFlowy
synced 2026-05-24 09:38:25 +00:00
feat: implement input service(alpha)
This commit is contained in:
parent
c048c8f623
commit
155b675dbe
6 changed files with 537 additions and 65 deletions
|
|
@ -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<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ExpandableFab> createState() => _ExpandableFabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpandableFabState extends State<ExpandableFab>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
late final Animation<double> _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<Widget> _buildExpandingActionButtons() {
|
||||||
|
final children = <Widget>[];
|
||||||
|
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<double> 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:example/expandable_floating_action_button.dart';
|
||||||
import 'package:example/plugin/document_node_widget.dart';
|
import 'package:example/plugin/document_node_widget.dart';
|
||||||
import 'package:example/plugin/selected_text_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/text_with_heading_node_widget.dart';
|
||||||
|
|
@ -60,6 +61,7 @@ class MyHomePage extends StatefulWidget {
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
final RenderPlugins renderPlugins = RenderPlugins();
|
final RenderPlugins renderPlugins = RenderPlugins();
|
||||||
late EditorState _editorState;
|
late EditorState _editorState;
|
||||||
|
int page = 0;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -80,53 +82,95 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
// the App.build method, and use it to set our appbar title.
|
// the App.build method, and use it to set our appbar title.
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
),
|
),
|
||||||
body: FutureBuilder<String>(
|
body: _buildBody(),
|
||||||
future: rootBundle.loadString('assets/document.json'),
|
floatingActionButton: ExpandableFab(
|
||||||
builder: (context, snapshot) {
|
distance: 112.0,
|
||||||
if (!snapshot.hasData) {
|
children: [
|
||||||
return const Center(
|
ActionButton(
|
||||||
child: CircularProgressIndicator(),
|
onPressed: () {
|
||||||
);
|
if (page == 0) return;
|
||||||
} else {
|
setState(() {
|
||||||
final data = Map<String, Object>.from(json.decode(snapshot.data!));
|
page = 0;
|
||||||
final document = StateTree.fromJson(data);
|
});
|
||||||
_editorState = EditorState(
|
},
|
||||||
document: document,
|
icon: const Icon(Icons.note_add),
|
||||||
renderPlugins: renderPlugins,
|
),
|
||||||
);
|
ActionButton(
|
||||||
return FlowyEditor(
|
onPressed: () {
|
||||||
editorState: _editorState,
|
if (page == 1) return;
|
||||||
keyEventHandlers: const [],
|
setState(() {
|
||||||
shortcuts: [
|
page = 1;
|
||||||
// TODO: this won't work, just a example for now.
|
});
|
||||||
{
|
},
|
||||||
'h1': (editorState, eventName) {
|
icon: const Icon(Icons.text_fields),
|
||||||
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 _buildBody() {
|
||||||
|
if (page == 0) {
|
||||||
|
return _buildFlowyEditor();
|
||||||
|
} else if (page == 1) {
|
||||||
|
return _buildTextfield();
|
||||||
|
}
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFlowyEditor() {
|
||||||
|
return FutureBuilder<String>(
|
||||||
|
future: rootBundle.loadString('assets/document.json'),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final data = Map<String, Object>.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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
|
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/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/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_nodes_handler.dart';
|
||||||
|
|
@ -36,22 +37,26 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
||||||
return FlowySelection(
|
return FlowySelection(
|
||||||
key: editorState.service.selectionServiceKey,
|
key: editorState.service.selectionServiceKey,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: FlowyKeyboard(
|
child: FlowyInput(
|
||||||
key: editorState.service.keyboardServiceKey,
|
key: editorState.service.inputServiceKey,
|
||||||
handlers: [
|
|
||||||
slashShortcutHandler,
|
|
||||||
flowyDeleteNodesHandler,
|
|
||||||
deleteSingleTextNodeHandler,
|
|
||||||
arrowKeysHandler,
|
|
||||||
...widget.keyEventHandlers,
|
|
||||||
],
|
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: FloatingShortcut(
|
child: FlowyKeyboard(
|
||||||
key: editorState.service.floatingShortcutServiceKey,
|
key: editorState.service.keyboardServiceKey,
|
||||||
size: const Size(200, 150), // TODO: support customize size.
|
handlers: [
|
||||||
|
slashShortcutHandler,
|
||||||
|
flowyDeleteNodesHandler,
|
||||||
|
deleteSingleTextNodeHandler,
|
||||||
|
arrowKeysHandler,
|
||||||
|
...widget.keyEventHandlers,
|
||||||
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
floatingShortcuts: widget.shortcuts,
|
child: FloatingShortcut(
|
||||||
child: editorState.build(context),
|
key: editorState.service.floatingShortcutServiceKey,
|
||||||
|
size: const Size(200, 150), // TODO: support customize size.
|
||||||
|
editorState: editorState,
|
||||||
|
floatingShortcuts: widget.shortcuts,
|
||||||
|
child: editorState.build(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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<TextEditingDelta> 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<FlowyInput> createState() => _FlowyInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlowyInputState extends State<FlowyInput>
|
||||||
|
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<TextEditingDelta> 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<String, dynamic> 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<TextEditingDelta> 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<TextNode>();
|
||||||
|
final text = textNodes.fold<String>(
|
||||||
|
'', (sum, textNode) => '$sum${textNode.toRawString()}\n');
|
||||||
|
attach(
|
||||||
|
TextEditingValue(
|
||||||
|
text: text,
|
||||||
|
selection: TextSelection(
|
||||||
|
baseOffset: selection.start.offset,
|
||||||
|
extentOffset: selection.end.offset,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,8 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||||
/// Returns the currently selected [Node]s.
|
/// Returns the currently selected [Node]s.
|
||||||
///
|
///
|
||||||
/// The order of the return is determined according to the selected order.
|
/// The order of the return is determined according to the selected order.
|
||||||
List<Node> get currentSelectedNodes;
|
ValueNotifier<List<Node>> get currentSelectedNodes;
|
||||||
|
Selection? get currentSelection;
|
||||||
|
|
||||||
/// ------------------ Selection ------------------------
|
/// ------------------ Selection ------------------------
|
||||||
|
|
||||||
|
|
@ -112,7 +113,10 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||||
EditorState get editorState => widget.editorState;
|
EditorState get editorState => widget.editorState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Node> currentSelectedNodes = [];
|
Selection? currentSelection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ValueNotifier<List<Node>> currentSelectedNodes = ValueNotifier([]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Node> getNodesInSelection(Selection selection) =>
|
List<Node> getNodesInSelection(Selection selection) =>
|
||||||
|
|
@ -292,7 +296,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearSelection() {
|
void _clearSelection() {
|
||||||
currentSelectedNodes = [];
|
currentSelection = null;
|
||||||
|
currentSelectedNodes.value = [];
|
||||||
|
|
||||||
// clear selection
|
// clear selection
|
||||||
_selectionOverlays
|
_selectionOverlays
|
||||||
|
|
@ -312,7 +317,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||||
final nodes =
|
final nodes =
|
||||||
_selectedNodesInSelection(editorState.document.root, selection);
|
_selectedNodesInSelection(editorState.document.root, selection);
|
||||||
|
|
||||||
currentSelectedNodes = nodes;
|
currentSelection = selection;
|
||||||
|
currentSelectedNodes.value = nodes;
|
||||||
|
|
||||||
var index = 0;
|
var index = 0;
|
||||||
for (final node in nodes) {
|
for (final node in nodes) {
|
||||||
|
|
@ -374,7 +380,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSelectedNodes = [node];
|
currentSelection = Selection.collapsed(position);
|
||||||
|
currentSelectedNodes.value = [node];
|
||||||
|
|
||||||
final selectable = node.selectable;
|
final selectable = node.selectable;
|
||||||
final rect = selectable?.getCursorRectInPosition(position);
|
final rect = selectable?.getCursorRectInPosition(position);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ class FlowyService {
|
||||||
// keyboard service
|
// keyboard service
|
||||||
final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service');
|
final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service');
|
||||||
|
|
||||||
|
// input service
|
||||||
|
final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service');
|
||||||
|
|
||||||
// floating shortcut service
|
// floating shortcut service
|
||||||
final floatingShortcutServiceKey =
|
final floatingShortcutServiceKey =
|
||||||
GlobalKey(debugLabel: 'flowy_floating_shortcut_service');
|
GlobalKey(debugLabel: 'flowy_floating_shortcut_service');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue