Skip to content

Commit

Permalink
feat! clipboard events support for iOS (#455)
Browse files Browse the repository at this point in the history
  • Loading branch information
knopp authored Oct 18, 2024
1 parent 55a57af commit 2785811
Show file tree
Hide file tree
Showing 17 changed files with 673 additions and 14 deletions.
43 changes: 40 additions & 3 deletions super_clipboard/lib/src/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'reader.dart';
import 'writer.dart';
import 'writer_data_provider.dart';
import 'system_clipboard.dart';
export 'package:super_native_extensions/raw_clipboard.dart' show TextEvent;

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

@override
Future<void> write(Iterable<DataWriterItem> items) async {
items.withHandlesSync((handles) async {
_event.write(handles);
});
final token = _event.beginWrite();
if (_event.isSynchronous) {
items.withHandlesSync((handles) async {
_event.write(token, handles);
});
} else {
items.withHandles((handles) async {
_event.write(token, handles);
});
}
}
}

Expand Down Expand Up @@ -134,3 +142,32 @@ class ClipboardEvents {
static final _cutEventListeners =
<void Function(ClipboardWriteEvent event)>[];
}

class TextEvents {
TextEvents._() {
raw.ClipboardEvents.instance.registerTextEventListener(_onTextEvent);
}

/// Returns clipboard events instance if available on current platform.
/// This is only supported on web, on other platforms use [SystemClipboard.instance]
/// to access the clipboard.
static TextEvents get instance => TextEvents._();

void registerTextEventListener(bool Function(raw.TextEvent) listener) {
_textEventListeners.add(listener);
}

void unregisterTextEventListener(bool Function(raw.TextEvent) listener) {
_textEventListeners.remove(listener);
}

bool _onTextEvent(raw.TextEvent event) {
bool handled = false;
for (final listener in _textEventListeners) {
handled |= listener(event);
}
return handled;
}

static final _textEventListeners = <bool Function(raw.TextEvent event)>[];
}
91 changes: 91 additions & 0 deletions super_native_extensions/ios/Classes/SuperNativeExtensionsPlugin.m
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
#import "SuperNativeExtensionsPlugin.h"

#include <objc/runtime.h>

extern void super_native_extensions_init(void);
extern bool super_native_extensions_text_input_plugin_cut(void);
extern bool super_native_extensions_text_input_plugin_copy(void);
extern bool super_native_extensions_text_input_plugin_paste(void);
extern bool super_native_extensions_text_input_plugin_select_all(void);

static void swizzleTextInputPlugin();

@implementation SuperNativeExtensionsPlugin

+ (void)initialize {
super_native_extensions_init();
swizzleTextInputPlugin();
}

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
Expand Down Expand Up @@ -59,3 +68,85 @@ - (void)relinquishPresentedItemToReader:
}

@end

@interface SNETextInputPlugin : NSObject
@end

@implementation SNETextInputPlugin

- (void)cut_:(id)sender {
if (!super_native_extensions_text_input_plugin_cut()) {
[self cut_:sender];
}
}

- (void)copy_:(id)sender {
if (!super_native_extensions_text_input_plugin_copy()) {
[self copy_:sender];
}
}

- (void)paste_:(id)sender {
if (!super_native_extensions_text_input_plugin_paste()) {
[self paste_:sender];
}
}

- (void)selectAll_:(id)sender {
if (!super_native_extensions_text_input_plugin_select_all()) {
[self selectAll_:sender];
}
}

@end

static void swizzle(SEL originalSelector, Class originalClass,
SEL replacementSelector, Class replacementClass) {
Method origMethod = class_getInstanceMethod(originalClass, originalSelector);

if (!origMethod) {
#if DEBUG
NSLog(@"Original method %@ not found for class %s",
NSStringFromSelector(originalSelector), class_getName(originalClass));
#endif
return;
}

Method altMethod =
class_getInstanceMethod(replacementClass, replacementSelector);
if (!altMethod) {
#if DEBUG
NSLog(@"Alternate method %@ not found for class %s",
NSStringFromSelector(replacementSelector),
class_getName(originalClass));
#endif
return;
}

class_addMethod(
originalClass, originalSelector,
class_getMethodImplementation(originalClass, originalSelector),
method_getTypeEncoding(origMethod));
class_addMethod(
originalClass, replacementSelector,
class_getMethodImplementation(replacementClass, replacementSelector),
method_getTypeEncoding(altMethod));

method_exchangeImplementations(
class_getInstanceMethod(originalClass, originalSelector),
class_getInstanceMethod(originalClass, replacementSelector));
}

static void swizzleTextInputPlugin() {
Class cls = NSClassFromString(@"FlutterTextInputView");
if (cls == nil) {
NSLog(@"FlutterTextInputPlugin not found");
return;
}

Class replacement = [SNETextInputPlugin class];
swizzle(@selector(cut:), cls, @selector(cut_:), replacement);
swizzle(@selector(copy:), cls, @selector(copy_:), replacement);
swizzle(@selector(paste:), cls, @selector(paste_:), replacement);
swizzle(@selector(selectAll:), cls, @selector(selectAll_:), replacement);
}
12 changes: 11 additions & 1 deletion super_native_extensions/lib/src/clipboard_events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ abstract class ClipboardReadEvent {
}

abstract class ClipboardWriteEvent {
void write(List<DataProviderHandle> providers);
bool get isSynchronous;
Object beginWrite(); // Returns token
void write(Object token, List<DataProviderHandle> providers);
}

enum TextEvent {
selectAll,
}

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

void unregisterCutEventListener(void Function(ClipboardWriteEvent) listener);

void registerTextEventListener(bool Function(TextEvent) listener);

void unregisterTextEventListener(bool Function(TextEvent) listener);
}
137 changes: 128 additions & 9 deletions super_native_extensions/lib/src/native/clipboard_events.dart
Original file line number Diff line number Diff line change
@@ -1,30 +1,149 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:irondash_message_channel/irondash_message_channel.dart';

import '../clipboard_events.dart';
import '../clipboard_reader.dart';
import '../clipboard_writer.dart';
import '../data_provider.dart';
import '../reader.dart';
import 'context.dart';

class _ClipboardWriteEvent extends ClipboardWriteEvent {
final _completers = <Completer>[];

@override
void write(Object token, List<DataProviderHandle> providers) async {
final completer = token as Completer;
await ClipboardWriter.instance.write(providers);
completer.complete();
}

@override
Object beginWrite() {
final completer = Completer();
_completers.add(completer);
return completer;
}

@override
bool get isSynchronous => false;
}

class _ClipboardReadEvent extends ClipboardReadEvent {
_ClipboardReadEvent(this.reader);

final DataReader reader;
bool didGetReader = false;

@override
DataReader getReader() {
didGetReader = true;
return reader;
}
}

class ClipboardEventsImpl extends ClipboardEvents {
ClipboardEventsImpl() {
_channel.setMethodCallHandler(_onMethodCall);
_channel.invokeMethod('newClipboardEventsManager');
}

Future<dynamic> _onMethodCall(MethodCall call) async {
if (call.method == 'copy') {
final writeEvent = _ClipboardWriteEvent();
for (final listener in _copyEventListeners) {
listener(writeEvent);
}
if (writeEvent._completers.isNotEmpty) {
await Future.wait(writeEvent._completers.map((e) => e.future));
return true;
} else {
return false;
}
} else if (call.method == 'cut') {
final writeEvent = _ClipboardWriteEvent();
for (final listener in _cutEventListeners) {
listener(writeEvent);
}
if (writeEvent._completers.isNotEmpty) {
await Future.wait(writeEvent._completers.map((e) => e.future));
return true;
} else {
return false;
}
} else if (call.method == 'paste') {
final reader = await ClipboardReader.instance.newClipboardReader();
final writeEvent = _ClipboardReadEvent(reader);
for (final listener in _pasteEventListeners) {
listener(writeEvent);
}
return writeEvent.didGetReader;
} else if (call.method == 'selectAll') {
bool handled = false;
for (final listener in _textEventListeners) {
handled |= listener(TextEvent.selectAll);
}
return handled;
}
}

@override
bool get supported => false;
bool get supported => defaultTargetPlatform == TargetPlatform.iOS;

final _pasteEventListeners = <void Function(ClipboardReadEvent reader)>[];
final _copyEventListeners = <void Function(ClipboardWriteEvent reader)>[];
final _cutEventListeners = <void Function(ClipboardWriteEvent reader)>[];
final _textEventListeners = <bool Function(TextEvent)>[];

@override
void registerPasteEventListener(
void Function(ClipboardReadEvent p1) listener) {}
void Function(ClipboardReadEvent p1) listener) {
_pasteEventListeners.add(listener);
}

@override
void unregisterPasteEventListener(
void Function(ClipboardReadEvent p1) listener) {}
void Function(ClipboardReadEvent p1) listener) {
_pasteEventListeners.remove(listener);
}

@override
void registerCopyEventListener(
void Function(ClipboardWriteEvent p1) listener) {}
void Function(ClipboardWriteEvent p1) listener) {
_copyEventListeners.add(listener);
}

@override
void registerCutEventListener(
void Function(ClipboardWriteEvent p1) listener) {}
void unregisterCopyEventListener(
void Function(ClipboardWriteEvent p1) listener) {
_copyEventListeners.remove(listener);
}

@override
void unregisterCopyEventListener(
void Function(ClipboardWriteEvent p1) listener) {}
void registerCutEventListener(
void Function(ClipboardWriteEvent p1) listener) {
_cutEventListeners.add(listener);
}

@override
void unregisterCutEventListener(
void Function(ClipboardWriteEvent p1) listener) {}
void Function(ClipboardWriteEvent p1) listener) {
_cutEventListeners.remove(listener);
}

@override
void registerTextEventListener(bool Function(TextEvent) listener) {
_textEventListeners.add(listener);
}

@override
void unregisterTextEventListener(bool Function(TextEvent) listener) {
_textEventListeners.remove(listener);
}

final _channel = NativeMethodChannel('ClipboardEventManager',
context: superNativeExtensionsContext);
}
17 changes: 16 additions & 1 deletion super_native_extensions/lib/src/web/clipboard_events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ class _PasteEvent extends ClipboardReadEvent {
class _WriteEvent extends ClipboardWriteEvent {
_WriteEvent({required this.event});

@override
Object beginWrite() {
// Not needed for synchronous events;
return const Object();
}

@override
bool get isSynchronous => true;

void _setData(String type, Object? data) {
if (data is! String) {
throw UnsupportedError('HTML Clipboard event only supports String data.');
Expand All @@ -42,7 +51,7 @@ class _WriteEvent extends ClipboardWriteEvent {
}

@override
void write(List<DataProviderHandle> providers) {
void write(Object token, List<DataProviderHandle> providers) {
event.preventDefault();
for (final provider in providers) {
for (final repr in provider.provider.representations) {
Expand Down Expand Up @@ -150,4 +159,10 @@ class ClipboardEventsImpl extends ClipboardEvents {
void Function(ClipboardWriteEvent p1) listener) {
_cutEventListeners.remove(listener);
}

@override
void registerTextEventListener(bool Function(TextEvent) listener) {}

@override
void unregisterTextEventListener(bool Function(TextEvent) listener) {}
}
Loading

0 comments on commit 2785811

Please sign in to comment.