@@ -16,7 +16,9 @@ public class SuperEditorClipboardPlugin: NSObject, FlutterPlugin {
1616 let instance = SuperEditorClipboardPlugin ( )
1717 registrar. addMethodCallDelegate ( instance, channel: channel)
1818
19+ // Swizzle both the action execution (paste) and the validation (canPerformAction)
1920 swizzleFlutterPaste ( )
21+ swizzleCanPerformAction ( )
2022 }
2123
2224 public func handle( _ call: FlutterMethodCall , result: @escaping FlutterResult ) {
@@ -33,63 +35,127 @@ public class SuperEditorClipboardPlugin: NSObject, FlutterPlugin {
3335 }
3436 }
3537
38+ // MARK: - Swizzling Logic
39+
3640 private static func swizzleFlutterPaste( ) {
37- // 1. Locate the private Flutter engine class
38- guard let flutterClass = NSClassFromString ( " FlutterTextInputView " ) else {
39- log ( " RichPastePlugin: Could not find FlutterTextInputView " )
41+ swizzle (
42+ clsName: " FlutterTextInputView " ,
43+ originalSelector: #selector( UIResponder . paste ( _: ) ) ,
44+ customSelector: #selector( customPaste ( _: ) )
45+ )
46+ }
47+
48+ private static func swizzleCanPerformAction( ) {
49+ swizzle (
50+ clsName: " FlutterTextInputView " ,
51+ originalSelector: #selector( UIResponder . canPerformAction ( _: withSender: ) ) ,
52+ customSelector: #selector( customCanPerformAction ( _: withSender: ) )
53+ )
54+ }
55+
56+ private static func swizzle( clsName: String , originalSelector: Selector , customSelector: Selector ) {
57+ guard let flutterClass = NSClassFromString ( clsName) else {
58+ log ( " Could not find \( clsName) " )
4059 return
4160 }
4261
43- let originalSelector = #selector( UIResponder . paste ( _: ) )
44- let swizzledSelector = #selector( customPaste ( _: ) )
45-
46- // 2. Get the methods
4762 guard let originalMethod = class_getInstanceMethod ( flutterClass, originalSelector) ,
48- let swizzledMethod = class_getInstanceMethod ( SuperEditorClipboardPlugin . self, swizzledSelector) else {
63+ let swizzledMethod = class_getInstanceMethod ( SuperEditorClipboardPlugin . self, customSelector) else {
64+ log ( " Could not find methods to swizzle for \( clsName) " )
4965 return
5066 }
5167
52- // 3. Inject our custom method into the Flutter engine class
68+ // Add the custom method to the Flutter class
5369 let didAddMethod = class_addMethod (
5470 flutterClass,
55- swizzledSelector ,
71+ customSelector ,
5672 method_getImplementation ( swizzledMethod) ,
5773 method_getTypeEncoding ( swizzledMethod)
5874 )
5975
6076 if didAddMethod {
61- // 4. Swap the pointers
62- let newMethod = class_getInstanceMethod ( flutterClass, swizzledSelector) !
77+ // Exchange implementations so 'originalSelector' calls our custom code,
78+ // and 'customSelector' calls the original code.
79+ let newMethod = class_getInstanceMethod ( flutterClass, customSelector) !
6380 method_exchangeImplementations ( originalMethod, newMethod)
81+ log ( " Successfully swizzled \( originalSelector) in \( clsName) " )
82+ } else {
83+ log ( " Failed to add method \( customSelector) to \( clsName) " )
6484 }
6585 }
6686
67- // This method is "moved" into FlutterTextInputView at runtime.
68- // 'self' inside this method will actually be the FlutterTextInputView instance.
87+ // MARK: - Custom Implementations
88+
89+ /// This method replaces `paste(_:)` at runtime.
6990 @objc func customPaste( _ sender: Any ? ) {
7091 if ( !SuperEditorClipboardPlugin. doCustomPaste) {
7192 SuperEditorClipboardPlugin . log ( " Running regular Flutter paste " )
72- // FALLBACK:
73- // This calls the ORIGINAL paste logic.
74- // Because we swapped the methods, calling 'customPaste' on 'self'
75- // now triggers the engine's original 'insertText' flow.
93+ // FALLBACK: Call original implementation (which is now mapped to customPaste)
7694 if self . responds ( to: #selector( customPaste ( _: ) ) ) {
7795 self . perform ( #selector( customPaste ( _: ) ) , with: sender)
7896 }
79-
80- return ;
97+ return
8198 }
8299
83100 SuperEditorClipboardPlugin . log ( " Running custom paste " )
84101 SuperEditorClipboardPlugin . channel? . invokeMethod ( " paste " , arguments: nil )
85102 }
86103
104+ /// This method replaces `canPerformAction(_:withSender:)` at runtime.
105+ @objc func customCanPerformAction( _ action: Selector , withSender sender: Any ? ) -> Bool {
106+ let isPasteAction = action == #selector( UIResponderStandardEditActions . paste ( _: ) )
107+
108+ // 1. If it is the PASTE action AND we are in custom mode, check our broader conditions.
109+ if isPasteAction && SuperEditorClipboardPlugin . doCustomPaste {
110+ // Check for ANY pasteable content (Images, Colors, URLs, Strings)
111+ // Note: Flutter only checks `hasStrings`.
112+ if UIPasteboard . general. hasStrings ||
113+ UIPasteboard . general. hasImages ||
114+ UIPasteboard . general. hasURLs ||
115+ UIPasteboard . general. hasColors {
116+ return true
117+ }
118+ }
119+
120+ // 2. Otherwise (or if the custom check failed), fall back to the ORIGINAL logic.
121+ // Because we exchanged implementations, calling 'customCanPerformAction' here
122+ // actually invokes the original Flutter engine logic.
123+
124+ // We cannot use 'perform' for Bool return types, so we use IMP casting.
125+ return SuperEditorClipboardPlugin . callOriginalCanPerformAction (
126+ instance: self ,
127+ selector: #selector( customCanPerformAction ( _: withSender: ) ) ,
128+ action: action,
129+ sender: sender
130+ )
131+ }
132+
133+ // MARK: - Helpers
134+
135+ /// Safely invokes the original implementation of `canPerformAction` (which is now swapped).
136+ private static func callOriginalCanPerformAction( instance: Any , selector: Selector , action: Selector , sender: Any ? ) -> Bool {
137+ guard let method = class_getInstanceMethod ( object_getClass ( instance) , selector) else {
138+ return false
139+ }
140+
141+ let imp = method_getImplementation ( method)
142+
143+ // Define the C function signature for (BOOL)objc_msgSend(id, SEL, SEL, id)
144+ typealias CanPerformActionFunction = @convention ( c) ( AnyObject , Selector , Selector , Any ? ) -> Bool
145+
146+ let originalFunction = unsafeBitCast ( imp, to: CanPerformActionFunction . self)
147+
148+ // 'instance' is 'self' (FlutterTextInputView)
149+ // 'selector' is the selector triggering this IMP (customCanPerformAction)
150+ // 'action' is the argument (e.g., paste:)
151+ return originalFunction ( instance as AnyObject , selector, action, sender)
152+ }
153+
87154 public static let isLoggingEnabled = false
88155
89156 internal static func log( _ message: String ) {
90157 if isLoggingEnabled {
91158 print ( " [SuperEditorClipboardPlugin] \( message) " )
92159 }
93160 }
94- }
95-
161+ }
0 commit comments