Skip to content

Commit 35b53c7

Browse files
committed
Image menu options for copy/remove
1 parent cfbd3d2 commit 35b53c7

File tree

7 files changed

+152
-36
lines changed

7 files changed

+152
-36
lines changed

example/lib/universal_ui/universal_ui.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class UniversalUI {
2525

2626
var ui = UniversalUI();
2727

28-
Widget defaultEmbedBuilderWeb(BuildContext context, Embed node, bool readOnly) {
28+
Widget defaultEmbedBuilderWeb(BuildContext context, QuillController controller,
29+
Embed node, bool readOnly) {
2930
switch (node.value.type) {
3031
case 'image':
3132
final imageUrl = node.value.data;

lib/src/widgets/controller.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import 'dart:math' as math;
22

33
import 'package:flutter/cupertino.dart';
4+
import 'package:flutter/services.dart';
45
import 'package:tuple/tuple.dart';
56

67
import '../models/documents/attribute.dart';
78
import '../models/documents/document.dart';
89
import '../models/documents/nodes/embeddable.dart';
10+
import '../models/documents/nodes/leaf.dart';
911
import '../models/documents/style.dart';
1012
import '../models/quill_delta.dart';
1113
import '../utils/delta.dart';
@@ -327,6 +329,21 @@ class QuillController extends ChangeNotifier {
327329
extentOffset: math.min(selection.extentOffset, end));
328330
}
329331

332+
/// Given offset, find its leaf node in document
333+
Leaf? queryNode(int offset) {
334+
return document.querySegmentLeafNode(offset).item2;
335+
}
336+
337+
/// Clipboard for image url
338+
String? _copiedImageUrl;
339+
340+
String? getCopiedImageUrl() => _copiedImageUrl;
341+
342+
set copiedImageUrl(String? value) {
343+
_copiedImageUrl = value;
344+
Clipboard.setData(const ClipboardData(text: ''));
345+
}
346+
330347
// Notify toolbar buttons directly with attributes
331348
Map<String, Attribute> toolbarButtonToggler = {};
332349
}

lib/src/widgets/delegate.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import 'package:flutter/material.dart';
55
import '../../flutter_quill.dart';
66
import 'text_selection.dart';
77

8-
typedef EmbedBuilder = Widget Function(
9-
BuildContext context, Embed node, bool readOnly);
8+
typedef EmbedBuilder = Widget Function(BuildContext context,
9+
QuillController controller, Embed node, bool readOnly);
1010

1111
typedef CustomStyleBuilder = TextStyle Function(Attribute attribute);
1212

lib/src/widgets/embeds/default_embed_builder.dart

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import 'dart:math';
2+
3+
import 'package:flutter/cupertino.dart';
14
import 'package:flutter/foundation.dart';
25
import 'package:flutter/material.dart';
36
import 'package:gallery_saver/gallery_saver.dart';
@@ -6,12 +9,14 @@ import '../../models/documents/nodes/leaf.dart' as leaf;
69
import '../../translations/toolbar.i18n.dart';
710
import '../../utils/platform.dart';
811
import '../../utils/string.dart';
12+
import '../controller.dart';
913
import 'image.dart';
14+
import 'image_resizer.dart';
1015
import 'video_app.dart';
1116
import 'youtube_video_app.dart';
1217

13-
Widget defaultEmbedBuilder(
14-
BuildContext context, leaf.Embed node, bool readOnly) {
18+
Widget defaultEmbedBuilder(BuildContext context, QuillController controller,
19+
leaf.Embed node, bool readOnly) {
1520
assert(!kIsWeb, 'Please provide EmbedBuilder for Web');
1621

1722
switch (node.value.type) {
@@ -40,34 +45,68 @@ Widget defaultEmbedBuilder(
4045
image ??= imageByUrl(imageUrl);
4146

4247
if (!readOnly && isMobile()) {
43-
// TODO: slider for width and height
44-
// return GestureDetector(
45-
// onTap: () {
46-
// showDialog(
47-
// context: context,
48-
// builder: (context) => Padding(
49-
// padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
50-
// child: SimpleDialog(
51-
// shape: const RoundedRectangleBorder(
52-
// borderRadius:
53-
// BorderRadius.all(Radius.circular(10))),
54-
// children: [
55-
// _SimpleDialogItem(
56-
// icon: Icons.settings_outlined,
57-
// color: Colors.lightBlueAccent,
58-
// text: 'Resize'.i18n,
59-
// onPressed: () {},
60-
// ),
61-
// _SimpleDialogItem(
62-
// icon: Icons.delete_forever_outlined,
63-
// color: Colors.red.shade200,
64-
// text: 'Remove'.i18n,
65-
// onPressed: () {},
66-
// )
67-
// ]),
68-
// ));
69-
// },
70-
// child: image);
48+
return GestureDetector(
49+
onTap: () {
50+
showDialog(
51+
context: context,
52+
builder: (context) {
53+
final resizeOption = _SimpleDialogItem(
54+
icon: Icons.settings_outlined,
55+
color: Colors.lightBlueAccent,
56+
text: 'Resize'.i18n,
57+
onPressed: () {
58+
Navigator.pop(context);
59+
showCupertinoModalPopup<void>(
60+
context: context,
61+
builder: (context) {
62+
return const ImageResizer();
63+
});
64+
},
65+
);
66+
final copyOption = _SimpleDialogItem(
67+
icon: Icons.copy_all_outlined,
68+
color: Colors.cyanAccent,
69+
text: 'Copy'.i18n,
70+
onPressed: () {
71+
var offset = controller.selection.start;
72+
var imageNode = controller.queryNode(offset);
73+
if (imageNode == null || !(imageNode is leaf.Embed)) {
74+
offset = max(0, offset - 1);
75+
imageNode = controller.queryNode(offset);
76+
}
77+
if (imageNode != null && imageNode is leaf.Embed) {
78+
final imageUrl = imageNode.value.data;
79+
controller.copiedImageUrl = imageUrl;
80+
}
81+
Navigator.pop(context);
82+
},
83+
);
84+
final removeOption = _SimpleDialogItem(
85+
icon: Icons.delete_forever_outlined,
86+
color: Colors.red.shade200,
87+
text: 'Remove'.i18n,
88+
onPressed: () {
89+
var offset = controller.selection.start;
90+
final imageNode = controller.queryNode(offset);
91+
if (imageNode == null || !(imageNode is leaf.Embed)) {
92+
offset = max(0, offset - 1);
93+
}
94+
controller.replaceText(offset, 1, '',
95+
TextSelection.collapsed(offset: offset));
96+
Navigator.pop(context);
97+
},
98+
);
99+
return Padding(
100+
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
101+
child: SimpleDialog(
102+
shape: const RoundedRectangleBorder(
103+
borderRadius:
104+
BorderRadius.all(Radius.circular(10))),
105+
children: [copyOption, removeOption]),
106+
);
107+
});
108+
},
109+
child: image);
71110
}
72111

73112
if (!readOnly || !isMobile() || isImageBase64(imageUrl)) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:flutter/cupertino.dart';
2+
import 'package:flutter/material.dart';
3+
4+
class ImageResizer extends StatefulWidget {
5+
const ImageResizer({Key? key}) : super(key: key);
6+
7+
@override
8+
_ImageResizerState createState() => _ImageResizerState();
9+
}
10+
11+
class _ImageResizerState extends State<ImageResizer> {
12+
@override
13+
Widget build(BuildContext context) {
14+
return CupertinoActionSheet(actions: [
15+
CupertinoActionSheetAction(
16+
onPressed: () {},
17+
child: Padding(
18+
padding: const EdgeInsets.symmetric(horizontal: 8),
19+
child: Card(
20+
child: Slider(
21+
value: 50,
22+
max: 100,
23+
divisions: 5,
24+
onChanged: (val) {},
25+
),
26+
)),
27+
),
28+
CupertinoActionSheetAction(
29+
onPressed: () {},
30+
child: Padding(
31+
padding: const EdgeInsets.symmetric(horizontal: 8),
32+
child: Card(
33+
child: Slider(
34+
value: 10,
35+
max: 100,
36+
divisions: 5,
37+
onChanged: (val) {},
38+
),
39+
)),
40+
)
41+
]);
42+
}
43+
}

lib/src/widgets/raw_editor.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:tuple/tuple.dart';
1515
import '../models/documents/attribute.dart';
1616
import '../models/documents/document.dart';
1717
import '../models/documents/nodes/block.dart';
18+
import '../models/documents/nodes/embeddable.dart';
1819
import '../models/documents/nodes/line.dart';
1920
import '../models/documents/nodes/node.dart';
2021
import '../models/documents/style.dart';
@@ -880,6 +881,7 @@ class RawEditorState extends EditorState
880881

881882
@override
882883
void copySelection(SelectionChangedCause cause) {
884+
widget.controller.copiedImageUrl = null;
883885
_pastePlainText = widget.controller.getPlainText();
884886
_pasteStyle = widget.controller.getAllIndividualSelectionStyles();
885887
// Copied straight from EditableTextState
@@ -904,8 +906,10 @@ class RawEditorState extends EditorState
904906

905907
@override
906908
void cutSelection(SelectionChangedCause cause) {
909+
widget.controller.copiedImageUrl = null;
907910
_pastePlainText = widget.controller.getPlainText();
908911
_pasteStyle = widget.controller.getAllIndividualSelectionStyles();
912+
909913
// Copied straight from EditableTextState
910914
super.cutSelection(cause);
911915
if (cause == SelectionChangedCause.toolbar) {
@@ -916,8 +920,19 @@ class RawEditorState extends EditorState
916920

917921
@override
918922
Future<void> pasteText(SelectionChangedCause cause) async {
923+
if (widget.controller.getCopiedImageUrl() != null) {
924+
final index = textEditingValue.selection.baseOffset;
925+
final length = textEditingValue.selection.extentOffset - index;
926+
widget.controller.replaceText(index, length,
927+
BlockEmbed.image(widget.controller.getCopiedImageUrl()!), null);
928+
widget.controller.copiedImageUrl = null;
929+
await Clipboard.setData(const ClipboardData(text: ''));
930+
return;
931+
}
932+
919933
// Copied straight from EditableTextState
920934
super.pasteText(cause); // ignore: unawaited_futures
935+
921936
if (cause == SelectionChangedCause.toolbar) {
922937
bringIntoView(textEditingValue.selection.extent);
923938
hideToolbar();

lib/src/widgets/text_line.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ class _TextLineState extends State<TextLine> {
133133
if (widget.line.hasEmbed && widget.line.childCount == 1) {
134134
// For video, it is always single child
135135
final embed = widget.line.children.single as Embed;
136-
return EmbedProxy(widget.embedBuilder(context, embed, widget.readOnly));
136+
return EmbedProxy(widget.embedBuilder(
137+
context, widget.controller, embed, widget.readOnly));
137138
}
138139
final textSpan = _getTextSpanForWholeLine(context);
139140
final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
@@ -173,8 +174,8 @@ class _TextLineState extends State<TextLine> {
173174
}
174175
// Here it should be image
175176
final embed = WidgetSpan(
176-
child: EmbedProxy(
177-
widget.embedBuilder(context, child, widget.readOnly)));
177+
child: EmbedProxy(widget.embedBuilder(
178+
context, widget.controller, child, widget.readOnly)));
178179
textSpanChildren.add(embed);
179180
continue;
180181
}

0 commit comments

Comments
 (0)