mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #750 from AppFlowy-IO/feat/copy-paste
Feat: paste rich text in flowy editor
This commit is contained in:
commit
3065f6d236
@ -90,8 +90,7 @@ class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) {
|
||||
// TODO: implement getPositionInOffset
|
||||
throw UnimplementedError();
|
||||
return Position(path: node.path, offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -6,9 +6,13 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <rich_clipboard_linux/rich_clipboard_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin");
|
||||
rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
rich_clipboard_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
@ -5,8 +5,10 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import rich_clipboard_macos
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
@ -1,20 +1,26 @@
|
||||
PODS:
|
||||
- FlutterMacOS (1.0.0)
|
||||
- rich_clipboard_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
rich_clipboard_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
|
||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||
|
||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
||||
|
@ -43,6 +43,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -57,6 +64,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flowy_editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -93,6 +107,13 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.15.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -177,6 +198,62 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
rich_clipboard:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rich_clipboard
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
rich_clipboard_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rich_clipboard_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
rich_clipboard_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rich_clipboard_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
rich_clipboard_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rich_clipboard_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
rich_clipboard_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rich_clipboard_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
rich_clipboard_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rich_clipboard_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
rich_clipboard_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rich_clipboard_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
rich_clipboard_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rich_clipboard_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -287,6 +364,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -296,4 +380,4 @@ packages:
|
||||
version: "6.1.0"
|
||||
sdks:
|
||||
dart: ">=2.17.0 <3.0.0"
|
||||
flutter: ">=2.11.0-0.1.pre"
|
||||
flutter: ">=3.0.0"
|
||||
|
@ -176,10 +176,11 @@ class TextNode extends Node {
|
||||
|
||||
TextNode({
|
||||
required super.type,
|
||||
required super.children,
|
||||
required super.attributes,
|
||||
required Delta delta,
|
||||
}) : _delta = delta;
|
||||
LinkedList<Node>? children,
|
||||
Attributes? attributes,
|
||||
}) : _delta = delta,
|
||||
super(children: children ?? LinkedList(), attributes: attributes ?? {});
|
||||
|
||||
TextNode.empty()
|
||||
: _delta = Delta([TextInsert(' ')]),
|
||||
|
@ -0,0 +1,149 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/document/text_delta.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as html;
|
||||
|
||||
class HTMLConverter {
|
||||
final html.Document _document;
|
||||
|
||||
HTMLConverter(String htmlString) : _document = parse(htmlString);
|
||||
|
||||
List<Node> toNodes() {
|
||||
final result = <Node>[];
|
||||
final delta = Delta();
|
||||
|
||||
final childNodes = _document.body?.nodes.toList() ?? <html.Node>[];
|
||||
for (final child in childNodes) {
|
||||
if (child is html.Element) {
|
||||
if (child.localName == "a" ||
|
||||
child.localName == "span" ||
|
||||
child.localName == "strong") {
|
||||
_handleRichTextElement(delta, child);
|
||||
} else {
|
||||
_handleElement(result, child);
|
||||
}
|
||||
} else {
|
||||
delta.insert(child.text ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
if (delta.operations.isNotEmpty) {
|
||||
result.add(TextNode(type: "text", delta: delta));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
_handleElement(List<Node> nodes, html.Element element) {
|
||||
if (element.localName == "h1") {
|
||||
_handleHeadingElement(nodes, element, "h1");
|
||||
} else if (element.localName == "h2") {
|
||||
_handleHeadingElement(nodes, element, "h2");
|
||||
} else if (element.localName == "h3") {
|
||||
_handleHeadingElement(nodes, element, "h3");
|
||||
} else if (element.localName == "ul") {
|
||||
_handleUnorderedList(nodes, element);
|
||||
} else if (element.localName == "li") {
|
||||
_handleListElement(nodes, element);
|
||||
} else if (element.localName == "p") {
|
||||
_handleParagraph(nodes, element);
|
||||
} else {
|
||||
final delta = Delta();
|
||||
delta.insert(element.text);
|
||||
if (delta.operations.isNotEmpty) {
|
||||
nodes.add(TextNode(type: "text", delta: delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleParagraph(List<Node> nodes, html.Element element) {
|
||||
_handleRichText(nodes, element);
|
||||
}
|
||||
|
||||
_handleRichTextElement(Delta delta, html.Element element) {
|
||||
if (element.localName == "span") {
|
||||
delta.insert(element.text);
|
||||
} else if (element.localName == "a") {
|
||||
final hyperLink = element.attributes["href"];
|
||||
Map<String, dynamic>? attributes;
|
||||
if (hyperLink != null) {
|
||||
attributes = {"href": hyperLink};
|
||||
}
|
||||
delta.insert(element.text, attributes);
|
||||
} else if (element.localName == "strong") {
|
||||
delta.insert(element.text, {"bold": true});
|
||||
}
|
||||
}
|
||||
|
||||
_handleRichText(List<Node> nodes, html.Element element) {
|
||||
final image = element.querySelector("img");
|
||||
if (image != null) {
|
||||
_handleImage(nodes, image);
|
||||
return;
|
||||
}
|
||||
|
||||
var delta = Delta();
|
||||
|
||||
for (final child in element.nodes.toList()) {
|
||||
if (child is html.Element) {
|
||||
if (child.localName == "a" ||
|
||||
child.localName == "span" ||
|
||||
child.localName == "strong") {
|
||||
_handleRichTextElement(delta, element);
|
||||
} else {
|
||||
delta.insert(child.text);
|
||||
}
|
||||
} else {
|
||||
delta.insert(child.text ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
if (delta.operations.isNotEmpty) {
|
||||
nodes.add(TextNode(type: "text", delta: delta));
|
||||
}
|
||||
}
|
||||
|
||||
_handleImage(List<Node> nodes, html.Element element) {
|
||||
final src = element.attributes["src"];
|
||||
final attributes = <String, dynamic>{};
|
||||
if (src != null) {
|
||||
attributes["image_src"] = src;
|
||||
}
|
||||
debugPrint("insert image: $src");
|
||||
nodes.add(
|
||||
Node(type: "image", attributes: attributes, children: LinkedList()));
|
||||
}
|
||||
|
||||
_handleUnorderedList(List<Node> nodes, html.Element element) {
|
||||
element.children.forEach((child) {
|
||||
_handleListElement(nodes, child);
|
||||
});
|
||||
}
|
||||
|
||||
_handleHeadingElement(
|
||||
List<Node> nodes,
|
||||
html.Element element,
|
||||
String headingStyle,
|
||||
) {
|
||||
final delta = Delta();
|
||||
delta.insert(element.text);
|
||||
if (delta.operations.isNotEmpty) {
|
||||
nodes.add(TextNode(
|
||||
type: "text",
|
||||
attributes: {"subtype": "heading", "heading": headingStyle},
|
||||
delta: delta));
|
||||
}
|
||||
}
|
||||
|
||||
_handleListElement(List<Node> nodes, html.Element element) {
|
||||
final childNodes = element.nodes.toList();
|
||||
for (final child in childNodes) {
|
||||
if (child is html.Element) {
|
||||
_handleRichText(nodes, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -80,6 +80,10 @@ class TransactionBuilder {
|
||||
add(TextEditOperation(path, delta, inverted));
|
||||
}
|
||||
|
||||
setAfterSelection(Selection sel) {
|
||||
afterSelection = sel;
|
||||
}
|
||||
|
||||
mergeText(TextNode firstNode, TextNode secondNode,
|
||||
{int? firstOffset, int secondOffset = 0}) {
|
||||
final firstLength = firstNode.delta.length;
|
||||
|
@ -7,17 +7,18 @@ 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/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/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_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/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/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/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/toolbar_service.dart';
|
||||
|
||||
NodeWidgetBuilders defaultBuilders = {
|
||||
@ -35,6 +36,7 @@ List<FlowyKeyEventHandler> defaultKeyEventHandler = [
|
||||
slashShortcutHandler,
|
||||
flowyDeleteNodesHandler,
|
||||
arrowKeysHandler,
|
||||
copyPasteKeysHandler,
|
||||
enterInEdgeOfTextNodeHandler,
|
||||
updateTextStyleByCommandXHandler,
|
||||
];
|
||||
|
@ -0,0 +1,180 @@
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flowy_editor/infra/html_converter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:rich_clipboard/rich_clipboard.dart';
|
||||
|
||||
_handleCopy() async {
|
||||
debugPrint('copy');
|
||||
}
|
||||
|
||||
_pasteHTML(EditorState editorState, String html) {
|
||||
final selection = editorState.cursorSelection;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final path = [...selection.end.path];
|
||||
if (path.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final converter = HTMLConverter(html);
|
||||
final nodes = converter.toNodes();
|
||||
|
||||
if (nodes.isEmpty) {
|
||||
return;
|
||||
} else if (nodes.length == 1) {
|
||||
final firstNode = nodes[0];
|
||||
final nodeAtPath = editorState.document.nodeAtPath(path)!;
|
||||
final tb = TransactionBuilder(editorState);
|
||||
final startOffset = selection.start.offset;
|
||||
if (nodeAtPath.type == "text" && firstNode.type == "text") {
|
||||
final textNodeAtPath = nodeAtPath as TextNode;
|
||||
final firstTextNode = firstNode as TextNode;
|
||||
tb.textEdit(textNodeAtPath,
|
||||
() => Delta().retain(startOffset).concat(firstTextNode.delta));
|
||||
tb.setAfterSelection(Selection.collapsed(Position(
|
||||
path: path, offset: startOffset + firstTextNode.delta.length)));
|
||||
tb.commit();
|
||||
}
|
||||
}
|
||||
|
||||
_pasteMultipleLinesInText(editorState, path, selection.start.offset, nodes);
|
||||
}
|
||||
|
||||
_pasteMultipleLinesInText(
|
||||
EditorState editorState, List<int> path, int offset, List<Node> nodes) {
|
||||
final tb = TransactionBuilder(editorState);
|
||||
|
||||
final firstNode = nodes[0];
|
||||
final nodeAtPath = editorState.document.nodeAtPath(path)!;
|
||||
|
||||
if (nodeAtPath.type == "text" && firstNode.type == "text") {
|
||||
// split and merge
|
||||
final textNodeAtPath = nodeAtPath as TextNode;
|
||||
final firstTextNode = firstNode as TextNode;
|
||||
final remain = textNodeAtPath.delta.slice(offset);
|
||||
|
||||
tb.textEdit(
|
||||
textNodeAtPath,
|
||||
() => Delta()
|
||||
.retain(offset)
|
||||
.delete(remain.length)
|
||||
.concat(firstTextNode.delta));
|
||||
|
||||
path[path.length - 1]++;
|
||||
final tailNodes = nodes.sublist(1);
|
||||
if (tailNodes.last.type == "text") {
|
||||
final tailTextNode = tailNodes.last as TextNode;
|
||||
tailTextNode.delta = tailTextNode.delta.concat(remain);
|
||||
} else if (remain.length > 0) {
|
||||
tailNodes.add(TextNode(type: "text", delta: remain));
|
||||
}
|
||||
|
||||
tb.insertNodes(path, tailNodes);
|
||||
tb.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
path[path.length - 1]++;
|
||||
tb.insertNodes(path, nodes);
|
||||
tb.commit();
|
||||
}
|
||||
|
||||
_handlePaste(EditorState editorState) async {
|
||||
final data = await RichClipboard.getData();
|
||||
if (data.html != null) {
|
||||
_pasteHTML(editorState, data.html!);
|
||||
return;
|
||||
}
|
||||
if (data.text != null) {
|
||||
_handlePastePlainText(editorState, data.text!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_handlePastePlainText(EditorState editorState, String plainText) {
|
||||
final selection = editorState.cursorSelection;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final lines = plainText
|
||||
.split("\n")
|
||||
.map((e) => e.replaceAll(RegExp(r'\r'), ""))
|
||||
.toList();
|
||||
|
||||
if (lines.isEmpty) {
|
||||
return;
|
||||
} else if (lines.length == 1) {
|
||||
final node =
|
||||
editorState.document.nodeAtPath(selection.end.path)! as TextNode;
|
||||
final beginOffset = selection.end.offset;
|
||||
TransactionBuilder(editorState)
|
||||
..textEdit(node, () => Delta().retain(beginOffset).insert(lines[0]))
|
||||
..setAfterSelection(Selection.collapsed(Position(
|
||||
path: selection.end.path, offset: beginOffset + lines[0].length)))
|
||||
..commit();
|
||||
} else {
|
||||
final firstLine = lines[0];
|
||||
final beginOffset = selection.end.offset;
|
||||
final remains = lines.sublist(1);
|
||||
|
||||
final path = [...selection.end.path];
|
||||
if (path.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final node =
|
||||
editorState.document.nodeAtPath(selection.end.path)! as TextNode;
|
||||
final insertedLineSuffix = node.delta.slice(beginOffset);
|
||||
|
||||
path[path.length - 1]++;
|
||||
var index = 0;
|
||||
final tb = TransactionBuilder(editorState);
|
||||
final nodes = remains.map((e) {
|
||||
if (index++ == remains.length - 1) {
|
||||
return TextNode(
|
||||
type: "text",
|
||||
delta: Delta().insert(e).addAll(insertedLineSuffix.operations));
|
||||
}
|
||||
return TextNode(type: "text", delta: Delta().insert(e));
|
||||
}).toList();
|
||||
// insert first line
|
||||
tb.textEdit(
|
||||
node,
|
||||
() => Delta()
|
||||
.retain(beginOffset)
|
||||
.insert(firstLine)
|
||||
.delete(node.delta.length - beginOffset));
|
||||
// insert remains
|
||||
tb.insertNodes(path, nodes);
|
||||
tb.commit();
|
||||
|
||||
// fixme: don't set the cursor manually
|
||||
editorState.updateCursorSelection(Selection.collapsed(
|
||||
Position(path: nodes.last.path, offset: lines.last.length)));
|
||||
}
|
||||
}
|
||||
|
||||
_handleCut() {
|
||||
debugPrint('cut');
|
||||
}
|
||||
|
||||
FlowyKeyEventHandler copyPasteKeysHandler = (editorState, event) {
|
||||
if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyC) {
|
||||
_handleCopy();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyV) {
|
||||
_handlePaste(editorState);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyX) {
|
||||
_handleCut();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
};
|
@ -11,6 +11,8 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
rich_clipboard: ^1.0.0
|
||||
html: ^0.15.0
|
||||
flutter_svg: ^1.1.1+1
|
||||
provider: ^6.0.3
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user