Skip to content

Commit 2785811

Browse files
authored
feat! clipboard events support for iOS (#455)
1 parent 55a57af commit 2785811

File tree

17 files changed

+673
-14
lines changed

17 files changed

+673
-14
lines changed

super_clipboard/lib/src/events.dart

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'reader.dart';
44
import 'writer.dart';
55
import 'writer_data_provider.dart';
66
import 'system_clipboard.dart';
7+
export 'package:super_native_extensions/raw_clipboard.dart' show TextEvent;
78

89
/// Event dispatched during a browser paste action (only available on web).
910
/// Allows reading data from clipboard.
@@ -44,9 +45,16 @@ class ClipboardWriteEvent extends ClipboardWriter {
4445

4546
@override
4647
Future<void> write(Iterable<DataWriterItem> items) async {
47-
items.withHandlesSync((handles) async {
48-
_event.write(handles);
49-
});
48+
final token = _event.beginWrite();
49+
if (_event.isSynchronous) {
50+
items.withHandlesSync((handles) async {
51+
_event.write(token, handles);
52+
});
53+
} else {
54+
items.withHandles((handles) async {
55+
_event.write(token, handles);
56+
});
57+
}
5058
}
5159
}
5260

@@ -134,3 +142,32 @@ class ClipboardEvents {
134142
static final _cutEventListeners =
135143
<void Function(ClipboardWriteEvent event)>[];
136144
}
145+
146+
class TextEvents {
147+
TextEvents._() {
148+
raw.ClipboardEvents.instance.registerTextEventListener(_onTextEvent);
149+
}
150+
151+
/// Returns clipboard events instance if available on current platform.
152+
/// This is only supported on web, on other platforms use [SystemClipboard.instance]
153+
/// to access the clipboard.
154+
static TextEvents get instance => TextEvents._();
155+
156+
void registerTextEventListener(bool Function(raw.TextEvent) listener) {
157+
_textEventListeners.add(listener);
158+
}
159+
160+
void unregisterTextEventListener(bool Function(raw.TextEvent) listener) {
161+
_textEventListeners.remove(listener);
162+
}
163+
164+
bool _onTextEvent(raw.TextEvent event) {
165+
bool handled = false;
166+
for (final listener in _textEventListeners) {
167+
handled |= listener(event);
168+
}
169+
return handled;
170+
}
171+
172+
static final _textEventListeners = <bool Function(raw.TextEvent event)>[];
173+
}

super_native_extensions/ios/Classes/SuperNativeExtensionsPlugin.m

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
#import "SuperNativeExtensionsPlugin.h"
22

3+
#include <objc/runtime.h>
4+
35
extern void super_native_extensions_init(void);
6+
extern bool super_native_extensions_text_input_plugin_cut(void);
7+
extern bool super_native_extensions_text_input_plugin_copy(void);
8+
extern bool super_native_extensions_text_input_plugin_paste(void);
9+
extern bool super_native_extensions_text_input_plugin_select_all(void);
10+
11+
static void swizzleTextInputPlugin();
412

513
@implementation SuperNativeExtensionsPlugin
614

715
+ (void)initialize {
816
super_native_extensions_init();
17+
swizzleTextInputPlugin();
918
}
1019

1120
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
@@ -59,3 +68,85 @@ - (void)relinquishPresentedItemToReader:
5968
}
6069

6170
@end
71+
72+
@interface SNETextInputPlugin : NSObject
73+
@end
74+
75+
@implementation SNETextInputPlugin
76+
77+
- (void)cut_:(id)sender {
78+
if (!super_native_extensions_text_input_plugin_cut()) {
79+
[self cut_:sender];
80+
}
81+
}
82+
83+
- (void)copy_:(id)sender {
84+
if (!super_native_extensions_text_input_plugin_copy()) {
85+
[self copy_:sender];
86+
}
87+
}
88+
89+
- (void)paste_:(id)sender {
90+
if (!super_native_extensions_text_input_plugin_paste()) {
91+
[self paste_:sender];
92+
}
93+
}
94+
95+
- (void)selectAll_:(id)sender {
96+
if (!super_native_extensions_text_input_plugin_select_all()) {
97+
[self selectAll_:sender];
98+
}
99+
}
100+
101+
@end
102+
103+
static void swizzle(SEL originalSelector, Class originalClass,
104+
SEL replacementSelector, Class replacementClass) {
105+
Method origMethod = class_getInstanceMethod(originalClass, originalSelector);
106+
107+
if (!origMethod) {
108+
#if DEBUG
109+
NSLog(@"Original method %@ not found for class %s",
110+
NSStringFromSelector(originalSelector), class_getName(originalClass));
111+
#endif
112+
return;
113+
}
114+
115+
Method altMethod =
116+
class_getInstanceMethod(replacementClass, replacementSelector);
117+
if (!altMethod) {
118+
#if DEBUG
119+
NSLog(@"Alternate method %@ not found for class %s",
120+
NSStringFromSelector(replacementSelector),
121+
class_getName(originalClass));
122+
#endif
123+
return;
124+
}
125+
126+
class_addMethod(
127+
originalClass, originalSelector,
128+
class_getMethodImplementation(originalClass, originalSelector),
129+
method_getTypeEncoding(origMethod));
130+
class_addMethod(
131+
originalClass, replacementSelector,
132+
class_getMethodImplementation(replacementClass, replacementSelector),
133+
method_getTypeEncoding(altMethod));
134+
135+
method_exchangeImplementations(
136+
class_getInstanceMethod(originalClass, originalSelector),
137+
class_getInstanceMethod(originalClass, replacementSelector));
138+
}
139+
140+
static void swizzleTextInputPlugin() {
141+
Class cls = NSClassFromString(@"FlutterTextInputView");
142+
if (cls == nil) {
143+
NSLog(@"FlutterTextInputPlugin not found");
144+
return;
145+
}
146+
147+
Class replacement = [SNETextInputPlugin class];
148+
swizzle(@selector(cut:), cls, @selector(cut_:), replacement);
149+
swizzle(@selector(copy:), cls, @selector(copy_:), replacement);
150+
swizzle(@selector(paste:), cls, @selector(paste_:), replacement);
151+
swizzle(@selector(selectAll:), cls, @selector(selectAll_:), replacement);
152+
}

super_native_extensions/lib/src/clipboard_events.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ abstract class ClipboardReadEvent {
1111
}
1212

1313
abstract class ClipboardWriteEvent {
14-
void write(List<DataProviderHandle> providers);
14+
bool get isSynchronous;
15+
Object beginWrite(); // Returns token
16+
void write(Object token, List<DataProviderHandle> providers);
17+
}
18+
19+
enum TextEvent {
20+
selectAll,
1521
}
1622

1723
abstract class ClipboardEvents {
@@ -32,4 +38,8 @@ abstract class ClipboardEvents {
3238
void registerCutEventListener(void Function(ClipboardWriteEvent) listener);
3339

3440
void unregisterCutEventListener(void Function(ClipboardWriteEvent) listener);
41+
42+
void registerTextEventListener(bool Function(TextEvent) listener);
43+
44+
void unregisterTextEventListener(bool Function(TextEvent) listener);
3545
}
Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,149 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/foundation.dart';
4+
import 'package:flutter/services.dart';
5+
import 'package:irondash_message_channel/irondash_message_channel.dart';
6+
17
import '../clipboard_events.dart';
8+
import '../clipboard_reader.dart';
9+
import '../clipboard_writer.dart';
10+
import '../data_provider.dart';
11+
import '../reader.dart';
12+
import 'context.dart';
13+
14+
class _ClipboardWriteEvent extends ClipboardWriteEvent {
15+
final _completers = <Completer>[];
16+
17+
@override
18+
void write(Object token, List<DataProviderHandle> providers) async {
19+
final completer = token as Completer;
20+
await ClipboardWriter.instance.write(providers);
21+
completer.complete();
22+
}
23+
24+
@override
25+
Object beginWrite() {
26+
final completer = Completer();
27+
_completers.add(completer);
28+
return completer;
29+
}
30+
31+
@override
32+
bool get isSynchronous => false;
33+
}
34+
35+
class _ClipboardReadEvent extends ClipboardReadEvent {
36+
_ClipboardReadEvent(this.reader);
37+
38+
final DataReader reader;
39+
bool didGetReader = false;
40+
41+
@override
42+
DataReader getReader() {
43+
didGetReader = true;
44+
return reader;
45+
}
46+
}
247

348
class ClipboardEventsImpl extends ClipboardEvents {
49+
ClipboardEventsImpl() {
50+
_channel.setMethodCallHandler(_onMethodCall);
51+
_channel.invokeMethod('newClipboardEventsManager');
52+
}
53+
54+
Future<dynamic> _onMethodCall(MethodCall call) async {
55+
if (call.method == 'copy') {
56+
final writeEvent = _ClipboardWriteEvent();
57+
for (final listener in _copyEventListeners) {
58+
listener(writeEvent);
59+
}
60+
if (writeEvent._completers.isNotEmpty) {
61+
await Future.wait(writeEvent._completers.map((e) => e.future));
62+
return true;
63+
} else {
64+
return false;
65+
}
66+
} else if (call.method == 'cut') {
67+
final writeEvent = _ClipboardWriteEvent();
68+
for (final listener in _cutEventListeners) {
69+
listener(writeEvent);
70+
}
71+
if (writeEvent._completers.isNotEmpty) {
72+
await Future.wait(writeEvent._completers.map((e) => e.future));
73+
return true;
74+
} else {
75+
return false;
76+
}
77+
} else if (call.method == 'paste') {
78+
final reader = await ClipboardReader.instance.newClipboardReader();
79+
final writeEvent = _ClipboardReadEvent(reader);
80+
for (final listener in _pasteEventListeners) {
81+
listener(writeEvent);
82+
}
83+
return writeEvent.didGetReader;
84+
} else if (call.method == 'selectAll') {
85+
bool handled = false;
86+
for (final listener in _textEventListeners) {
87+
handled |= listener(TextEvent.selectAll);
88+
}
89+
return handled;
90+
}
91+
}
92+
493
@override
5-
bool get supported => false;
94+
bool get supported => defaultTargetPlatform == TargetPlatform.iOS;
95+
96+
final _pasteEventListeners = <void Function(ClipboardReadEvent reader)>[];
97+
final _copyEventListeners = <void Function(ClipboardWriteEvent reader)>[];
98+
final _cutEventListeners = <void Function(ClipboardWriteEvent reader)>[];
99+
final _textEventListeners = <bool Function(TextEvent)>[];
6100

7101
@override
8102
void registerPasteEventListener(
9-
void Function(ClipboardReadEvent p1) listener) {}
103+
void Function(ClipboardReadEvent p1) listener) {
104+
_pasteEventListeners.add(listener);
105+
}
10106

11107
@override
12108
void unregisterPasteEventListener(
13-
void Function(ClipboardReadEvent p1) listener) {}
109+
void Function(ClipboardReadEvent p1) listener) {
110+
_pasteEventListeners.remove(listener);
111+
}
14112

15113
@override
16114
void registerCopyEventListener(
17-
void Function(ClipboardWriteEvent p1) listener) {}
115+
void Function(ClipboardWriteEvent p1) listener) {
116+
_copyEventListeners.add(listener);
117+
}
18118

19119
@override
20-
void registerCutEventListener(
21-
void Function(ClipboardWriteEvent p1) listener) {}
120+
void unregisterCopyEventListener(
121+
void Function(ClipboardWriteEvent p1) listener) {
122+
_copyEventListeners.remove(listener);
123+
}
22124

23125
@override
24-
void unregisterCopyEventListener(
25-
void Function(ClipboardWriteEvent p1) listener) {}
126+
void registerCutEventListener(
127+
void Function(ClipboardWriteEvent p1) listener) {
128+
_cutEventListeners.add(listener);
129+
}
26130

27131
@override
28132
void unregisterCutEventListener(
29-
void Function(ClipboardWriteEvent p1) listener) {}
133+
void Function(ClipboardWriteEvent p1) listener) {
134+
_cutEventListeners.remove(listener);
135+
}
136+
137+
@override
138+
void registerTextEventListener(bool Function(TextEvent) listener) {
139+
_textEventListeners.add(listener);
140+
}
141+
142+
@override
143+
void unregisterTextEventListener(bool Function(TextEvent) listener) {
144+
_textEventListeners.remove(listener);
145+
}
146+
147+
final _channel = NativeMethodChannel('ClipboardEventManager',
148+
context: superNativeExtensionsContext);
30149
}

super_native_extensions/lib/src/web/clipboard_events.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ class _PasteEvent extends ClipboardReadEvent {
3434
class _WriteEvent extends ClipboardWriteEvent {
3535
_WriteEvent({required this.event});
3636

37+
@override
38+
Object beginWrite() {
39+
// Not needed for synchronous events;
40+
return const Object();
41+
}
42+
43+
@override
44+
bool get isSynchronous => true;
45+
3746
void _setData(String type, Object? data) {
3847
if (data is! String) {
3948
throw UnsupportedError('HTML Clipboard event only supports String data.');
@@ -42,7 +51,7 @@ class _WriteEvent extends ClipboardWriteEvent {
4251
}
4352

4453
@override
45-
void write(List<DataProviderHandle> providers) {
54+
void write(Object token, List<DataProviderHandle> providers) {
4655
event.preventDefault();
4756
for (final provider in providers) {
4857
for (final repr in provider.provider.representations) {
@@ -150,4 +159,10 @@ class ClipboardEventsImpl extends ClipboardEvents {
150159
void Function(ClipboardWriteEvent p1) listener) {
151160
_cutEventListeners.remove(listener);
152161
}
162+
163+
@override
164+
void registerTextEventListener(bool Function(TextEvent) listener) {}
165+
166+
@override
167+
void unregisterTextEventListener(bool Function(TextEvent) listener) {}
153168
}

0 commit comments

Comments
 (0)