ホームページ>開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでリッチテキストを扱う・アンドゥを試す(改訂版)
Xojo / Real Studio Trial and Error
目次
CocoaのDeclareでリッチテキストを扱う・アンドゥを試す(改訂版)
- はじめに
- テキスト入力のアンドゥ
- ペースト(ドロップ)処理を見直す
- ペースト(ドロップ)時のアンドゥ
- サンプルの仕様
- Xojoでの実装(例1)
- Xojoでの実装(例2)
- おわりに
- お世話になったサイト
- 更新履歴
はじめに
以下は、Xojo Cocoaビルドについての話題です。
NSTextViewのアンドゥを試してみました。
なお検証には、Xojo 2015 Release 4.1を用いています。(Mac mini mid 2010 + OS X 10.11.4 El Capitan)
注1)アンドゥ機能は、リッチテキストに限定されるものではありませんが、話題の流れ上、ここに置いています。
注2)初出時に懸案となっていた事項を解決する目処が立ちましたので、それらをもとに全面改訂しました。(旧版はこちら)
テキスト入力のアンドゥ
Cocoaは標準でアンドゥの仕組み(NSUndoManager)を持っていて、NSTextViewからも利用できます。
しかもテキスト関連については、NSTextViewがNSUndoManagerへの登録を内部で行ってくれるため、操作ごとに自分で登録作業をする必要はありません。
なので、設定をオンにするだけで(デフォルトで設定がオンになっているので)何もしなくても、文字入力だけでなく、文字修飾や段落設定等も含めたアンドゥが実現できます。(2016.06.28訂正)
一方、文字入力に関しては、一連の文字列の入力がアンドゥの単位となるため、使いにくいというケースも考えられます。
(OS標準のテキストエディットがそうなっているので、アンドゥしてみると分かります。)
そこで、一文字ずつアンドゥする方法がないか調べたところ、以下のサイトで公開されていました。
参考サイト(1):UITextViewで一文字ずつUndo/Redoする方法 - Qiita
Xcodeで上記サイトのコードを試してみた(注3)ところ、期待通りのものであることが確認できました。
注3)iOS用なのでOSX用に、replaceRange:withText:を、shouldChangeTextInRange:replacementString:に変更した。さて、上記サイトのコードではMethod Swizzlingという手法が使われています。
(Method Swizzlingについては、ググると、(iOS向けも含めて)有益な情報をご提供頂いている様々なサイトがヒットします。)
この手法では、クラスの拡張に、カテゴリの機能を利用しています。
問題は、Xojoでカテゴリをどう扱うか、ですが、これにはお馴染みのObjective-CのランタイムAPIを用いた動的クラス生成を利用します。
ランタイムAPIは、既にDelegateやTarget/Actionの実装に利用していますが、以下のサイトによると、カテゴリとしても利用できるということです。
(というか、class_addMethodsはカテゴリの仕組みを実現するために用意された、ようだ。)
参考サイト(2):ダイナミックObjective-C (28) ランタイムAPIでさらに動的に(2) - メソッドの追加 | マイナビニュース
なお、既存のクラスを拡張する場合は、そのクラスに対してclass_addMethodsしてやればいいので、クラスの生成は不要となります。
メソッド生成時に指定するType Encodingsは、オリジナルのメソッドの値をそのままコピーするのがベターと思われます。また、メソッドの入れ替え登録部分(参考サイト(1)の例ではsetupHook())は、まんまランタイムAPIなので、こちらも問題なく実装できます。
取得には、例えば以下のサイトの関数を使わせて頂くと便利です。
参考サイト(3):Safx: NSObjectを継承しないクラスをランタイムAPIで作成する方法について
ペースト(ドロップ)処理を見直す
アンドゥの前に、まず、画像を含むテキストのペースト(ドロップもペーストの一種)処理それ自体を見直すことにしました。
と言うのも、現状では、以下の制限が確認されているからです。(他にもあるかも。)
調べたところ、Deleteキーでの削除は、DropObjectイベントの代わりにNSTextViewネイティブのメソッドであるperformDragOperation:を使用することで、(Deleteキー処理を自分で実装しなくても)対応できることが分かりました。
- Deleteキーで、画像を削除できない
- メニュー(コンテキストメニュー含む)のカットは機能するが、ペーストは不十分(画像単体ではペーストされず、文字と混在した場合は文字だけペーストされる)
- プレビュー等の別アプリでコピーした画像をペーストできない
また、ペーストメニューについては、paste:メソッドをオーバーライドして自分で実装する(ペーストボードから画像または文字列を抽出してしまえば、後はドロップと同じ処理)ことにより、(機能チェックが不十分なので、途上版レベルではありますが)対応できました。
参考サイト(4):iOS 8リマインダーアプリの挙動調査 その2 - ObjecTips(iOS用なので、一部読み替えが必要)
参考サイト(5):Accessing Attributes(iOS用ですが、趣旨は同じ)
別アプリでコピーした画像をペーストできない点については、仕様ですと云ってしまってもいいかも。なお、performDragOperation:とpaste:のオーバーライドには、こちらもMethod Swizzlingを利用します。
ペースト(ドロップ)時のアンドゥ
ペースト(ドロップ)時のアンドゥ登録は自動では行われませんので、自分で行うことになります。
参考サイト(6):(旧) Cocoaの日々: rubberBand(その19)Undoの実装 / NSUndoManager
上記サイトにあるように、アンドゥだけならsetAttributedString:メソッドとペースト(ドロップ)前のデータを登録するだけで事足りました。
(以下は、前回プロジェクト>TextArea1のDropObjectイベント>最後の部分、による例)
ですが、これだけだとリドゥができません。(しかも、場合によっては異常終了します。)// 画像を追加したストレージを書き戻す(既存分) Declare Sub setAttributedString Lib "Cocoa" Selector "setAttributedString:" (receiver As Ptr, identifier As Ptr) // Undo登録用に現在のデータを複製しておく(追加分) Declare Function mutableCopy Lib "Cocoa" Selector "mutableCopy" (receiver As Ptr) As Ptr Dim oldAttrString As Ptr = mutableCopy(attrString) // NSUndoManagerの取得(追加分) declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt11 As Ptr = undoManager(Window1.Handle) // UndoManagerにUndo時に使うメソッドとデータを登録(追加分) declare function prepareWithInvocationTarget lib "Cocoa" selector "prepareWithInvocationTarget:" (receiver as Ptr, target As Ptr) As Ptr Dim pnt12 As Ptr = prepareWithInvocationTarget(pnt11, pnt2) setAttributedString(pnt12, oldAttrString) setAttributedString(pnt2, attrString) // (既存分)
リドゥに対応させるには、これも上記サイトにあるように、アンドゥ登録したメソッド(ここではsetAttributedString:)内でもNSUndoManagerへの登録処理が必要になります。
setAttributedString:のオーバーライドには、Method Swizzlingを使う手もありそうですが、ここでは、setAttributedString:とリドゥ用の登録処理を記述したラッパーメソッドを用意して、それをNSUndoManagerに登録することとしました。
なお、ラッパーメソッドはXojoのメソッドなので、NSUndoManagerに直接登録することはできない(と思われる)ため、こちらもObjective-CのランタイムAPIを用いた動的クラス生成を使用します。
サンプルの仕様
以上を踏まえて今回は、設定をオンにするだけのシンプルなものと、一文字ずつ&ペースト(ドロップ)処理のアンドゥが可能なものを試してみることにしました。
それぞれの仕様は以下の通りとしました。
追記:例1の内容は誤りではありませんが、リドゥメニューを追加するだけで、他は何もしなくても同等の機能が実現できることを確認しました。(2016.06.28)例1(シンプル)
例2(一文字ずつ&ペースト(ドロップ)処理のアンドゥ)
- リッチテキストのプロジェクトをベースとする
- NSTextViewのアンドゥをオン(setAllowsUndo:YES)にする
- メニューハンドラを実装して、アンドゥ/リドゥ処理を記述
- EnableMenuItemsイベントを実装して、アンドゥ/リドゥの終点に達したら当該メニューをDisableにする
- 例1をベースとする
- 画像ドロップ処理をDropObjectイベントからperformDragOperation:メソッドに変更
- paste:メソッドをオーバーライドして、画像を含むテキストのペーストを実装
- 一文字ずつのアンドゥ登録を追加
- ペースト(ドロップ)時のアンドゥ登録を追加
- setAttributedString:をオーバーライドしてペースト(ドロップ)時のリドゥ登録を追加
Xojoでの実装(例1)
実行してみたところ、アンドゥが機能することを確認しました。
- 前回プロジェクトをベースとする
- MainMenuBarのEditMenuにEditRedoを追加
- 以下をWindow1のEnableMenuItemsイベントに記述
// NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) declare function canUndo lib "Cocoa" selector "canUndo" (receiver as Ptr) as Boolean // Return BOOL if canUndo(pnt1) then EditUndo.Enabled=true else EditUndo.Enabled=false end if declare function canRedo lib "Cocoa" selector "canRedo" (receiver as Ptr) as Boolean // Return BOOL if canRedo(pnt1) then EditRedo.Enabled=true else EditRedo.Enabled=false end if
- 以下をWindow1のOpenイベントに追加(前回プロジェクトで記述したコードの後に追加)
// Delegate設定 Declare Sub setDelegate Lib "Cocoa" Selector "setDelegate:" (receiver As Ptr, id As Ptr) setDelegate(pnt1, nil) // なぜか設定しないとUndoできない。Delegate先はnilで構わない。 // Undoを有効に declare sub setAllowsUndo lib "Cocoa" selector "setAllowsUndo:" (receiver as Ptr, flg As Boolean) setAllowsUndo(pnt1, true)
- 以下をWindow1のメニューハンドラ(Menu Handlers)に追加
メニュー項目名: EditRedo declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) declare sub redo lib "Cocoa" selector "redo" (receiver as Ptr) redo(pnt1) Return True
- 以下をWindow1のメニューハンドラ(Menu Handlers)に追加
メニュー項目名: EditUndo declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) declare sub undo lib "Cocoa" selector "undo" (receiver as Ptr) undo(pnt1) Return True
Xojoでの実装(例2)
実行してみたところ、アンドゥおよび画像を含むペースト(ドロップ)操作やDeleteキーによる画像削除が機能することを確認しました。
- 例1のプロジェクトをベースとする
- TextArea1のDropObjectイベントとOpenイベントを削除
- 以下をWindow1のOpenイベントに追加(既プロジェクトで記述したコードの後に追加)
// UndoManagerに登録するメソッドを動的に生成 RegisterMethod() // カテゴリ機能を使ってメソッドをNSTextViewに追加 RegisterMethodNSTextView() // メソッドの入れ替え setupHook()
- 以下をWindow1の共有メソッド(Shared Methods)に追加(不具合が確認されたので、書き直しました。詳細はこちら。 )
注1)テストが不十分な途上版なので、参考扱いです。メソッド名: myPaste 引数: id As Ptr, SEL As CString Ptr, sender As Ptr Dim flg1 As Boolean = false Dim flg2 As Boolean = false // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr // ペーストボードの取得 Dim pnt1 As Ptr = NSClassFromString("NSPasteboard") Declare Function generalPasteboard Lib "Cocoa" Selector "generalPasteboard" (receiver As Ptr) As Ptr Dim pboard As Ptr = generalPasteboard(pnt1) // ペーストボード内に含まれるタイプを順に取得 Declare Function types Lib "Cocoa" Selector "types" (receiver As Ptr) As Ptr Dim ary As Ptr = types(pboard) Declare Function count Lib "Cocoa" Selector "count" (receiver As Ptr) As Integer Dim cnt As Integer = count(ary) Declare Function objectAtIndexString Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, idx As Integer) As CFStringRef for i As Integer = 0 to cnt-1 if objectAtIndexString(ary, i) = "com.apple.flat-rtfd" then // 画像を含むタイプが見つかったらフラグをオンに flg1 = true exit end if next Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr if flg1 then// 画像を含んでいるので独自のペースト処理 // com.apple.flat-rtfdからNSAttributedStringを復元 Declare Function dataForType Lib "Cocoa" Selector "dataForType:" (receiver As Ptr, type As CFStringRef) As Ptr Dim data As Ptr = dataForType(pboard, "com.apple.flat-rtfd") Dim option As Ptr = NSClassFromString("NSMutableDictionary") Declare Function dictionary Lib "Cocoa" Selector "dictionary" (receiver As Ptr) As Ptr option = dictionary(option) Declare Sub setObject Lib "Cocoa" Selector "setObject:forKey:" (receiver As Ptr, obj As CFStringRef, key As CFStringRef) setObject(option, "NSRTFDTextDocumentType", "NSDocumentTypeDocumentAttribute") Dim atstr As Ptr = NSClassFromString("NSAttributedString") atstr = alloc(atstr) Declare Function dataInit Lib "Cocoa" Selector "initWithData:options:documentAttributes:error:" (receiver As Ptr, data As Ptr, optn As Ptr, attr As Ptr, err As Ptr) As Ptr atstr = dataInit(atstr, data, option, nil, nil) Declare Function myString Lib "Cocoa" Selector "string" (receiver As Ptr) As CFStringRef Dim str As CFStringRef = myString(atstr) // 文字列 Declare Function length Lib "Cocoa" Selector "length" (receiver As Ptr) As Integer Dim ll As Integer = length(atstr) // 文字列長 // 個々のアトリビュートごとに処理 Dim attrib As Ptr Dim rng As NSRange rng.location = 0 rng.length = 0 Dim attachment As Ptr Dim wrap As Ptr Dim atstr2 As Ptr = NSClassFromString("NSAttributedString") atstr2 = alloc(atstr2) do until (rng.location + rng.length) >= ll Declare Function attributesAtIndex Lib "Cocoa" Selector "attributesAtIndex:effectiveRange:" (receiver As Ptr, idx As Integer, byRef rng As NSRange) As Ptr // NSRangeをbyRef指定することで戻り値が取得できる attrib = attributesAtIndex(atstr, (rng.location + rng.length), rng) // アトリビュートが適用される範囲がrngに返ってくる Declare Function objectForKey Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As Ptr attachment = objectForKey(attrib, "NSAttachment") // NSAttachmentAttributeNameでは取れないので、NSAttachmentを指定 if attachment = nil then // 文字列 Declare Function dataInit Lib "Cocoa" Selector "initWithString:attributes:" (receiver As Ptr, str As CFStringRef, attr As Ptr) As Ptr atstr2 = dataInit(atstr2, Mid(str, rng.location+1, rng.length), attrib) // アトリビュートが適用される範囲の文字列を取得(文字単位となるので、MidBではなくMidを使う) insTextStrage("String", id, atstr2) // TextStrageに文字列を挿入 flg2 = true // 文字列の挿入が終了した else // 画像 Declare Function fileWrapper Lib "Cocoa" Selector "fileWrapper" (receiver As Ptr) As Ptr wrap = fileWrapper(attachment) if wrap <> nil then insTextStrage("Image", id, wrap) // TextStrageに画像を挿入 flg2 = true // 画像の挿入が終了した end if end if loop end if if not flg2 then // 独自のペースト処理対象外のアイテム // 本来のpaste:を実行 declare sub myPaste lib "Cocoa" selector "myPaste:" (receiver as Ptr, txt As Ptr) myPaste(id, sender) end if
注2)NSRangeをbyRef指定することで戻り値が取得できることが判ったため、書き直した。(2018.07.23)
注3)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
- 以下をWindow1の共有メソッド(Shared Methods)に追加
注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)メソッド名: myPerformDragOperation 引数: id As Ptr, SEL As CString, sender As Ptr // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function draggingPasteboard Lib "Cocoa" Selector "draggingPasteboard" (receiver As Ptr) As Ptr Dim pboard As Ptr = draggingPasteboard(sender) Declare Function types Lib "Cocoa" Selector "types" (receiver As Ptr) As Ptr Dim typ As Ptr = types(pboard) Declare Function containsObject Lib "Cocoa" Selector "containsObject:" (receiver As Ptr, obj As CFStringRef) As Boolean Dim folderItemAvailable As Boolean = containsObject(typ, "NSFilenamesPboardType") Dim ret As Boolean if folderItemAvailable then // フォルダアイテムがドロップされた // パスの取得(最初の一個目のみ) Declare Function propertyListForType Lib "Cocoa" Selector "propertyListForType:" (receiver As Ptr, obj As CFStringRef) As Ptr Dim ary As Ptr = propertyListForType(pboard, "NSFilenamesPboardType") Declare Function objectAtIndex Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, info As Integer) As CFStringRef Dim filepath As String = objectAtIndex(ary, 0) // フォルダアイテムの内容(画像)を取得 Dim wrap As Ptr = NSClassFromString("NSFileWrapper") Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr wrap = alloc(wrap) Declare Function pathInit Lib "Cocoa" Selector "initWithPath:" (receiver As Ptr, path As CFStringRef) As Ptr wrap = pathInit(wrap, filepath) // TextStrageに挿入 insTextStrage("Image", id, wrap) // 戻り値は常にtrue ret = true else // フォルダアイテム以外がドロップされた // 本来のperformDragOperation:を実行 declare function myPerformDragOperation lib "Cocoa" selector "myPerformDragOperation:" (receiver as Ptr, sender As Ptr) As Boolean ret = myPerformDragOperation(id, sender) end if // 処理結果を返す(trueでないとドロップを受け付けたことにならない?) return ret
- 以下をWindow1の共有メソッド(Shared Methods)に追加
メソッド名: insTextStrage 引数: kind As String, id as Ptr, obj As Ptr // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr // NSTextStorageの取得 declare function textStorage lib "Cocoa" selector "textStorage" (obj_id as Ptr) As Ptr // Return NSTextStorage* Dim pnt2 As Ptr = textStorage(id) Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr Dim attachChar As Ptr if kind = "String" then // 文字列 attachChar = obj elseif kind = "Image" then // 画像 Dim attachment As Ptr = NSClassFromString("NSTextAttachment") attachment = alloc(attachment) Declare Function wrapInit Lib "Cocoa" Selector "initWithFileWrapper:" (receiver As Ptr, wrapper As Ptr) As Ptr attachment = wrapInit(attachment, obj) Dim attachChar1 As Ptr = NSClassFromString("NSAttributedString") Declare Function attributedStringWithAttachment Lib "Cocoa" Selector "attributedStringWithAttachment:" (receiver As Ptr, attachment As Ptr) As Ptr attachChar = attributedStringWithAttachment(attachChar1, attachment) end if Dim attrString As Ptr = NSClassFromString("NSMutableAttributedString") attrString = alloc(attrString) Declare Function initWithString Lib "Cocoa" Selector "initWithAttributedString:" (receiver As Ptr, str As Ptr) As Ptr attrString = initWithString(attrString, pnt2) Declare Function mutableCopy Lib "Cocoa" Selector "mutableCopy" (receiver As Ptr) As Ptr Dim oldAttrString As Ptr = mutableCopy(attrString) // Undo登録用に現在の値を複製しておく Declare Sub beginEditing Lib "Cocoa" Selector "beginEditing" (receiver As Ptr) beginEditing(attrString) Declare Sub insertAttributedString Lib "Cocoa" Selector "insertAttributedString:atIndex:" (receiver As Ptr, attachChar As Ptr, index As Integer) insertAttributedString(attrString, attachChar, Window1.TextArea1.SelStart) // Xojo側からキャレット位置を取得。NSTextView or NSTextStorageから取得した方がいいかも。 Declare Sub endEditing Lib "Cocoa" Selector "endEditing" (receiver As Ptr) endEditing(attrString) // NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt11 As Ptr = undoManager(Window1.Handle) // UndoManagerにUndo時に使うメソッドと値を登録 declare function prepareWithInvocationTarget lib "Cocoa" selector "prepareWithInvocationTarget:" (receiver as Ptr, target As Ptr) As Ptr Dim pnt12 As Ptr = prepareWithInvocationTarget(pnt11, stAttrStrInstance) Declare Sub tStragesetAttrString Lib "Cocoa" Selector "tStrage:setAttrString:" (receiver As Ptr, tStrage As Ptr, attrString As Ptr) tStragesetAttrString(pnt12, pnt2, oldAttrString) // オブジェクトを追加したストレージを書き戻す mySetAttributedString(nil, nil, pnt2, attrString)
- 以下をWindow1の共有メソッド(Shared Methods)に追加
注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)メソッド名: myDeleteBackward 引数: id As Ptr, SEL As CString // NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) // UndoGrouping開始 declare sub beginUndoGrouping lib "Cocoa" selector "beginUndoGrouping" (receiver as Ptr) beginUndoGrouping(pnt1) // 本来のdeleteBackwardを実行 declare sub myDeleteBackward lib "Cocoa" selector "myDeleteBackward" (receiver as Ptr) myDeleteBackward(id) // UndoGrouping終了 declare sub endUndoGrouping lib "Cocoa" selector "endUndoGrouping" (receiver as Ptr) endUndoGrouping(pnt1)
- 以下をWindow1の共有メソッド(Shared Methods)に追加
注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)メソッド名: myInsertText 引数: id As Ptr, SEL As CString, txt As Ptr // NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) // UndoGrouping開始 declare sub beginUndoGrouping lib "Cocoa" selector "beginUndoGrouping" (receiver as Ptr) beginUndoGrouping(pnt1) // 本来のinsertText:を実行 declare sub myInsertText lib "Cocoa" selector "myInsertText:" (receiver as Ptr, txt As Ptr) myInsertText(id, txt) // UndoGrouping終了 declare sub endUndoGrouping lib "Cocoa" selector "endUndoGrouping" (receiver as Ptr) endUndoGrouping(pnt1)
- 以下をWindow1の共有メソッド(Shared Methods)に追加(不具合が確認されたので、書き直しました。詳細はこちら。)
注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)メソッド名: mySetAttributedString 引数: id As Ptr, SEL As CString, tStrage As Ptr, attrString As Ptr // NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt11 As Ptr = undoManager(Window1.Handle) // Redo用 declare function prepareWithInvocationTarget lib "Cocoa" selector "prepareWithInvocationTarget:" (receiver as Ptr, target As Ptr) As Ptr Dim pnt12 As Ptr = prepareWithInvocationTarget(pnt11, stAttrStrInstance) Declare Sub tStragesetAttrString Lib "Cocoa" Selector "tStrage:setAttrString:" (receiver As Ptr, tStrage As Ptr, attrString As Ptr) tStragesetAttrString(pnt12, tStrage, attrString) // 画像を追加したストレージを書き戻す Declare Sub setAttributedString Lib "Cocoa" Selector "setAttributedString:" (receiver As Ptr, identifier As Ptr) setAttributedString(tStrage, attrString)
- 以下をWindow1の共有メソッド(Shared Methods)に追加
注1)引数の型にNSRangeを指定しているが、これはcocoaのクラスではなく、構造体の名前(15項参照)。メソッド名: mySetMarkedTextSelectedRange 引数: id As Ptr, SEL As CString, txt As Ptr, range As NSRange // txtをPtr型で受けているため、CFStringRef型に変換(macoslibによる) soft declare function CFRetain lib "Carbon" (cf as Ptr) as CFStringRef Dim st As CFStringRef = CFRetain(txt) if 0 < LenB(st) then // NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) // UndoGrouping開始 declare sub beginUndoGrouping lib "Cocoa" selector "beginUndoGrouping" (receiver as Ptr) beginUndoGrouping(pnt1) // 本来のsetMarkedText:SelectedRange:を実行 declare sub mySetMarkedText lib "Cocoa" selector "mySetMarkedText:selectedRange:" (receiver as Ptr, txt As Ptr, range As NSRange) mySetMarkedText(id, txt, range) // UndoGrouping終了 declare sub endUndoGrouping lib "Cocoa" selector "endUndoGrouping" (receiver as Ptr) endUndoGrouping(pnt1) end if
注2)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
- 以下をWindow1の共有メソッド(Shared Methods)に追加
注1)引数の型にNSRangeを指定しているが、これはcocoaのクラスではなく、構造体の名前(15項参照)。メソッド名: myShouldChangeTextInRangeReplacementString 引数: id As Ptr, SEL As CString, range As NSRange, txt As Ptr // NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) // UndoGrouping開始 declare sub beginUndoGrouping lib "Cocoa" selector "beginUndoGrouping" (receiver as Ptr) beginUndoGrouping(pnt1) // 本来のshouldChangeTextInRange:replacementString:を実行 declare function myShouldChangeText lib "Cocoa" selector "myShouldChangeTextInRange:replacementString:" (receiver as Ptr, range As NSRange, txt As Ptr) As Boolean Dim ret As Boolean = myShouldChangeText(id, range, txt) // UndoGrouping終了 declare sub endUndoGrouping lib "Cocoa" selector "endUndoGrouping" (receiver as Ptr) endUndoGrouping(pnt1) // 本来のshouldChangeTextInRange:replacementString:の結果を返す(trueでないと文字が入力されない?) return ret
注2)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
- 以下をWindow1の共有メソッド(Shared Methods)に追加
メソッド名: RegisterMethod // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr // Declare宣言 Declare Function objc_allocateClassPair Lib "Cocoa" (superclass As Ptr, name As CString, extraBytes As Integer) as Ptr Declare Sub objc_registerClassPair Lib "Cocoa" (cls As Ptr) Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean // 既にインスタンス作成済なら戻る if stAttrStrInstance <> nil then return end if // クラス名をmyIdentifier、メタクラス名をNSObjectにして、生成 Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myIdentifier", 0) // ランタイムに登録(参照を可能とするため) objc_registerClassPair newClassId // UndoManagerから呼ばれるメソッドの受け口となるメソッドを追加(tStrage:setAttrString:をXojo側で用意したmySetAttributedStringメソッドで受け取る。) if not class_addMethod (newClassId, NSSelectorFromString("tStrage:setAttrString:"), AddressOf mySetAttributedString, "@@:@@") then msgBox "error." return end if // 上記で生成したクラスのインスタンスを作成 Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr Dim targetId As Ptr = init(alloc(newClassId)) // インスタンスを保持 stAttrStrInstance = targetId
- 以下をWindow1の共有メソッド(Shared Methods)に追加
メソッド名: RegisterMethodNSTextView // 既に設定済なら戻る if nstextviewDone then return end if nstextviewDone = true // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr Dim TextViewPtr As Ptr = NSClassFromString("NSTextView") // Declare宣言 Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean // 以下、一文字ずつアンドゥ用 // NSTextViewクラスにmyShouldChangeTextInRange:replacementString:メソッドを追加(Xojo側で用意したmyShouldChangeTextInRangeReplacementStringメソッドで受け取る。) if not class_addMethod (TextViewPtr, NSSelectorFromString("myShouldChangeTextInRange:replacementString:"), AddressOf myShouldChangeTextInRangeReplacementString, "c40@0:8{_NSRange=QQ}16@32") then msgBox "error1." return end if // NSTextViewクラスにmySetMarkedText:selectedRange:メソッドを追加(Xojo側で用意したmySetMarkedTextSelectedRangeメソッドで受け取る。) if not class_addMethod (TextViewPtr, NSSelectorFromString("mySetMarkedText:selectedRange:"), AddressOf mySetMarkedTextSelectedRange, "v40@0:8@16{_NSRange=QQ}24") then msgBox "error2." return end if // NSTextViewクラスにmyInsertText:メソッドを追加(Xojo側で用意したmyInsertTextメソッドで受け取る。) if not class_addMethod (TextViewPtr, NSSelectorFromString("myInsertText:"), AddressOf myInsertText, "v24@0:8@16") then msgBox "error3." return end if // NSTextViewクラスにmyDeleteBackwardメソッドを追加(Xojo側で用意したmyDeleteBackwardメソッドで受け取る。) if not class_addMethod (TextViewPtr, NSSelectorFromString("myDeleteBackward"), AddressOf myDeleteBackward, "v24@0:8@16") then msgBox "error4." return end if // 以下、ペースト(ドロップ)処理用 // NSTextViewクラスにmyPaste:メソッドを追加(Xojo側で用意したmyPasteメソッドで受け取る。) if not class_addMethod (TextViewPtr, NSSelectorFromString("myPaste:"), AddressOf myPaste, "v24@0:8@16") then msgBox "error6." return end if // NSTextViewクラスにmyPerformDragOperation:メソッドを追加(Xojo側で用意したmyPerformDragOperationメソッドで受け取る。) if not class_addMethod (TextViewPtr, NSSelectorFromString("myPerformDragOperation:"), AddressOf myPerformDragOperation, "c24@0:8@16") then msgBox "error5." return end if
- 以下をWindow1の共有メソッド(Shared Methods)に追加
メソッド名: setupHook // 既に設定済なら戻る if hookDone then return end if hookDone = true // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr Dim TextViewPtr As Ptr = NSClassFromString("NSTextView") declare function class_getInstanceMethod lib "Cocoa" ( clsID as Ptr, SEL as Ptr ) as Ptr declare sub method_exchangeImplementations lib "Cocoa" ( clsID1 as Ptr, clsID2 as Ptr ) // 以下、一文字ずつアンドゥ用 Dim orgMethod As Ptr = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("shouldChangeTextInRange:replacementString:")) Dim myMethod As Ptr = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("myShouldChangeTextInRange:replacementString:")) method_exchangeImplementations(orgMethod, myMethod) orgMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("setMarkedText:selectedRange:")) myMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("mySetMarkedText:selectedRange:")) method_exchangeImplementations(orgMethod, myMethod) orgMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("insertText:")) myMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("myInsertText:")) method_exchangeImplementations(orgMethod, myMethod) orgMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("deleteBackward")) myMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("myDeleteBackward")) method_exchangeImplementations(orgMethod, myMethod) // 以下、ペースト(ドロップ)処理用 orgMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("paste:")) myMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("myPaste:")) method_exchangeImplementations(orgMethod, myMethod) orgMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("performDragOperation:")) myMethod = class_getInstanceMethod(TextViewPtr, NSSelectorFromString("myPerformDragOperation:")) method_exchangeImplementations(orgMethod, myMethod)
- 以下をWindow1の共有プロパティ(Shared Properties)に追加
プロパティ名: hookDone データ型: Boolean 標準値: なし
- 以下をWindow1の共有プロパティ(Shared Properties)に追加
プロパティ名: nstextviewDone データ型: Boolean 標準値: なし
- 以下をWindow1の共有プロパティ(Shared Properties)に追加
プロパティ名: stAttrStrInstance データ型: Ptr 標準値: なし
- 他に、NSRange(構造体)が必要ですが、これはmacoslibからコピーさせて頂きました。
おわりに
一文字ずつのアンドゥの方は、日本語の変換中も逐一記録されるので、ちょっと気持ち悪いかも。
別アプリでコピーした画像のペーストについては、機会があればトライしてみたいと思います。
なお、今回も仕組みを理解するために、極力シンプルな書き方を心懸けています。
(例えば、allocした変数の一部は然るべきタイミングでReleaseするべきと思われますが、そのためにはコードを工夫する必要があります。)
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):UITextViewで一文字ずつUndo/Redoする方法 - Qiita
参考サイト(2):ダイナミックObjective-C (28) ランタイムAPIでさらに動的に(2) - メソッドの追加 | マイナビニュース
参考サイト(3):Safx: NSObjectを継承しないクラスをランタイムAPIで作成する方法について
参考サイト(4):iOS 8リマインダーアプリの挙動調査 その2 - ObjecTips
参考サイト(5):Accessing Attributes
参考サイト(6):(旧) Cocoaの日々: rubberBand(その19)Undoの実装 / NSUndoManager
更新履歴
2024.02.28 不具合が確認されたので、別記事(詳細はこちら)に対応策を纏めた。
2018.07.30 Xojoでの実装(例2)の4,5,7,8,9,10項を改訂
2018.07.23 Xojoでの実装(例2)の4項を改訂
2016.06.28 TextAreaはデフォルトでアンドゥが有効になっていることに基づいて、記事を加筆訂正。
2016.05.01 改訂版・新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]