Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat! clipboard events support for iOS #455

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading