ホームページ>開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareで変則オートコンプリートを試す
Xojo / Real Studio Trial and Error
目次
CocoaのDeclareで変則オートコンプリートを試す
はじめに
以下は、Xojo Cocoaビルドについての話題です。
テキスト入力時の単語補完(オートコンプリート)機能について、調べてみました。ただし、標準の使い方とは異なるため、「変則」としました。
(注:ブラウザーのオートコンプリートは仕組みが異なるため、本件とは別の話題となります。)
なお検証には、Xojo 2022 Release 4.1を用いています。(Mac mini 2018 + macOS 14.6.1 Sonoma)
方針
macOSには、標準でオートコンプリート機能が備わっています。
例えばテキストエディットで、Option+Escキー(またはF5キー。以下、F5キーと表記)を押すと候補が表示され、続けてリターン>スペースを繰り返していくと、何やら文が出来上がっていきます。
また、途中まで打ってF5キーを押すと、それに続く候補が表示されます。
参考サイト(1):Macのテキストエディットで単語補完を表示する - Apple サポート (日本)
ただし、これは英語のみで、日本語が含まれると全く表示されなくなります。
では、同等の機能を日本語でも、となると、これは一筋縄ではいかないことは容易に想像できます。(簡単なら、とっくに標準実装されているだろうし。)
ここでは、仕組みはそのまま利用して、あらかじめ登録しておいた候補を表示する、ことを目指します。
候補リストは、NSTextView(DesktopTextAreaの親クラス。以下、前者)とNSTextField(DesktopTextFieldの親クラス。以下、後者)で異なり、前者はNSTextViewDelegateのtextView:completions:forPartialWordRange:indexOfSelectedItem:メソッド、後者はNSControlTextEditingDelegateのcontrol:textView:completions:forPartialWordRange:indexOfSelectedItem:メソッドで返します。
これで、F5キーを押した時のリストが入れ替わります。(デフォルトのリストにしたい場合は、引数で渡ってきたリストをそのまま返します。)
今回は、この両者を試してみることにします。
前者はDesktopTextAreaとしますが、後者はNSTextFieldを継承したCocoaツールバーの検索フィールド(NSSearchField)を対象とします。
DesktopTextAreaには既にDelegateが設定済なので、置き換えてしまうと副作用が生じるため、以前にもやった既存のDelegateをカテゴリー拡張する方法で対処します。この場合、拡張は全てのDesktopTextAreaに適用されますが、それはそれでよしとします。(実験目的なので)
検索フィールドの方は既存のDelegateがありませんので、通常のランタイムAPIのインスタンスを用います。
候補リストは、あらかじめ作成しておいて常に同じものを表示する、としてもいいのですが、ここではその場で登録することも考えてみます。
登録のタイミングは、前者は文字列選択後にボタンを押すという明示的なものとし、後者はリターンキー押下時とします。
以上を踏まえ、(残りの)仕様は以下の通りとしました。
例1(DesktopTextArea)
例2(検索フィールド)
- 登録ボタンには、キーボードショートカット(Command + E)を割り当てる。
共通
- 以前作成したCocoaのDeclareでツールバーを実装する・Big Surの新機能を試すのプロジェクトをベースとする。
- 上記プロジェクトでは、検索フィールドにカスタムDelegateを(ランタイムAPIで)設定済なので、それに当該メソッドを追加する。
- スプリット化は無効にする。(後述するチェックボックスを置き易くするため。)
- 自作の候補リスト(カスタム)とデフォルトは、ウィンドウ内に配置したチェックボックスで切り替える。
- 候補リストは終了時にファイルに保存し、次回起動時に読み込む。
- 候補リストファイルはテキスト形式とし、別途編集可能とする。
Xojoでの実装(例1)
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、カスタムオートコンプリートが機能することを確認しました。
- Xojoで新規プロジェクトを作成
- Window1に、DesktopButton(Name:Button1, Caption:登録 ⌘E)、DesktopCheckBox(Name:CheckBox1, Caption:カスタムオートコンプリート)、DesktopTextArea(Name:TextArea1)を置く
- 以下をButton1にペースト(できなければ、Sub - Endの間をOpeningイベントに記述)
Sub Opening() Handles Opening // ショートカットキーの設定(Command + E) // command(NSEventModifierFlagCommand = 1 << 20)をセット Declare Sub setKeyEquivalentModifierMask Lib "Cocoa" Selector "setKeyEquivalentModifierMask:" (receiver As Ptr, key As UInteger) setKeyEquivalentModifierMask(me.Handle, Bitwise.ShiftLeft(1,20)) // eをセット(ケースセンシティブなので、小文字を指定) Declare Sub setKeyEquivalent Lib "Cocoa" Selector "setKeyEquivalent:" (receiver As Ptr, key As CFStringRef) setKeyEquivalent(me.Handle, "e") End Sub
- 以下をButton1にペースト(できなければ、Sub - Endの間をPressedイベントに記述)
Sub Pressed() Handles Pressed // 指定された文字列をリストに追加 AddAutoComp(TextArea1.SelectedText) End Sub
- 以下をCheckBox1にペースト(できなければ、Sub - Endの間をValueChangedイベントに記述)
Sub ValueChanged() Handles ValueChanged gCustomComp=me.Value End Sub
- 以下をWindow1にペースト(できなければ、Sub - Endの間をClosingイベントに記述)
Sub Closing() Handles Closing // カスタムオートコンプリート用リスト書き出し SaveAutoComp() End Sub
- 以下をWindow1にペースト(できなければ、Sub - Endの間をOpeningイベントに記述)
Sub Opening() Handles Opening // NSTextViewの取得 declare function documentView lib "Cocoa" selector "documentView" (receiver as Ptr) as Ptr // Return NSTextView* Dim tview As Ptr = documentView(TextArea1.Handle) // TextArea1 = NSScrollView // NSTextViewにDelegateを設定 Dim tvd As NSTextViewDelegate = new NSTextViewDelegate(tview) // カスタムオートコンプリート用リスト読み込み LoadAutoComp() End Sub
- 以下をWindow1にペースト
Protected Sub AddAutoComp(sword As String) // 空なら戻る if sword="" then return end if // 重複チェック for i As Integer = 0 to gAutoComp.Ubound if sword=gAutoComp(i) then return next // 検索文字列を追加 gAutoComp.Append sword End Sub
- 以下をWindow1にペースト
Protected Sub LoadAutoComp() Var f As FolderItem = GetFolderItem("").Child("autocomp.txt") if f=nil or f.Exists=false then return end if Var textInput As TextInputStream = TextInputStream.Open(f) textInput.Encoding = Encodings.UTF8 Var st As String ReDim gAutoComp(-1) do st = textInput.ReadLine gAutoComp.Append st loop until textInput.EndOfFile textInput.Close End Sub
- 以下をWindow1にペースト
Protected Sub SaveAutoComp() if gAutoComp.Ubound<0 then return end if Var f As FolderItem = GetFolderItem("").Child("autocomp.txt") if f=nil then return end if Var t As TextOutputStream = TextOutputStream.Create(f) for i As Integer = 0 to gAutoComp.Ubound t.WriteLine gAutoComp(i) next t.Close End Sub
- 新規クラスを作成(名前は、ここでは「NSTextViewDelegate」とした。)
- 以下をNSTextViewDelegateにペースト(できなければDelegatesに、名前:ActionDelegate、引数:id As Ptr, SEL As CString, textView As Ptr, words As Ptr, charRange As NSRange, index As Integer、を追加)
Private Function ActionDelegate(id As Ptr, SEL As CString, textView As Ptr, words As Ptr, charRange As NSRange, index As Integer) As Ptr
- 以下をNSTextViewDelegateにペースト
Public Sub Constructor(textView As Ptr) // Delegate設定 RegisterMethod() // レイズされたクラスメソッドをインスタンス側で処理するための仕込み ActionHandler = AddressOf MakeCompList End Sub
- 以下をNSTextViewDelegateにペースト
Protected Function MakeCompList(id As Ptr, SEL As CString, textView As Ptr, words As Ptr, charRange As NSRange, index As Integer) As Ptr // オートコンプリートのモードによって、返す配列を変える if gCustomComp then // Custom // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Dim arr As Ptr = NSClassFromString("NSMutableArray") // クラスメソッドなので、まずNSMutableArrayクラスを取得 Declare Function getArray Lib "Cocoa" Selector "array" (receiver As Ptr) As Ptr // Return Array* arr=getArray(arr) // gAutoCompからArrayを生成 Declare Sub addObject Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As CFStringRef) for i As Integer = 0 to gAutoComp.Ubound addObject(arr, gAutoComp(i)) next return arr else // Default return words end if End Function
- 以下をNSTextViewDelegateにペースト
Private Shared Sub RegisterMethod() // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr // XOJTextAreaControllerクラスの取得 Dim TxtCntlPtr As Ptr = NSClassFromString("XOJTextAreaController") // Declare宣言 Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean // Delegateの対象となるメソッドを追加(textView:completions:forPartialWordRange:indexOfSelectedItem:をXojo側で用意したtextViewCompletionsメソッドで受け取る。) if not class_addMethod (TxtCntlPtr, NSSelectorFromString("textView:completions:forPartialWordRange:indexOfSelectedItem:"), AddressOf textViewCompletions, "v24@0:8@16") then msgBox "error." return end if End Sub
- 以下をNSTextViewDelegateにペースト
Private Shared Function textViewCompletions(id As Ptr, SEL As CString, textView As Ptr, words As Ptr, charRange As NSRange, index As Integer) As Ptr // レイズされたクラスメソッドをインスタンスメソッドに渡す return ActionHandler.Invoke(id,SEL,textView,words,charRange,index) End Function
- 以下をNSTextViewDelegateにペースト(できなければShared Propertyに、Name:ActionHandler、Type:ActionDelegate、を追加)
Private Shared Property ActionHandler As ActionDelegate
- 新規モジュールを作成(名前は、ここでは「AutoComplete」とした。)
- 以下をAutoCompleteにペースト(できなければPropertyに、Name:gAutoComp(-1)、Type:String、を追加)
Public Property gAutoComp(-1) As String
- 以下をAutoCompleteにペースト(できなければPropertyに、Name:gCustomComp、Type:Boolean、を追加)
Public Property gCustomComp As Boolean
- 他に、NSRange(構造体)が必要ですが、それらはmacoslibからコピーさせて頂きました。(別途モジュールを用意してコピーする。)
注)macoslibではメソッドの引数、構造体のメンバーの型に、Singleが割り当てられているものがあるが、それらはCGFloatに書き換える。
Xojoでの実装(例2)
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、カスタムオートコンプリートが機能することを確認しました。
- CocoaのDeclareでツールバーを実装する・Big Surの新機能を試すのプロジェクトをベースとする
(注:このプロジェクトはXojo 2021 Release 1で作成したものであり、Xojo 2021 Release 3以降のAPIとは異なります。Xojo 2021 Release 3以降で新規に作成する場合は、イベント名等が異なりますので、例1を参考にする等して対応して下さい。)- Window1に、CheckBox(Name:CheckBox1, Caption:カスタムオートコンプリート)を追加
- 以下をCheckBox1にペースト
Sub Action() Handles Action gCustomComp=me.Value End Sub
- 以下をWindow1にペースト
Sub Close() Handles Close // カスタムオートコンプリート用リスト書き出し SaveAutoComp() End Sub
- Window1のOpenイベントを以下に差し替え
Sub Open() Handles Open // SplitViewとToolbar生成 InitToolbar(nil) // 今回はSplitViewは使わないので、nilを渡す // カスタムオートコンプリート用リスト読み込み LoadAutoComp() End Sub
- 以下をWindow1にペースト
Protected Sub AddAutoComp(sword As String) // 空なら戻る if sword="" then return end if // 重複チェック for i As Integer = 0 to gAutoComp.Ubound if sword=gAutoComp(i) then return next // 検索文字列を追加 gAutoComp.Append sword End Sub
- 以下をWindow1にペースト
Protected Sub LoadAutoComp() Var f As FolderItem = GetFolderItem("").Child("autocomp.txt") if f=nil or f.Exists=false then return end if Var textInput As TextInputStream = TextInputStream.Open(f) textInput.Encoding = Encodings.UTF8 Var st As String ReDim gAutoComp(-1) do st = textInput.ReadLine gAutoComp.Append st loop until textInput.EndOfFile textInput.Close End Sub
- 以下をWindow1にペースト
Protected Sub SaveAutoComp() if gAutoComp.Ubound<0 then return end if Var f As FolderItem = GetFolderItem("").Child("autocomp.txt") if f=nil then return end if Var t As TextOutputStream = TextOutputStream.Create(f) for i As Integer = 0 to gAutoComp.Ubound t.WriteLine gAutoComp(i) next t.Close End Sub
- Window1のToolbarItemClickedメソッドを以下に差し替え
Private Sub ToolbarItemClicked(sender As Ptr) Declare Function description Lib "Cocoa" Selector "description" (receiver As Ptr) As CFStringRef Dim name As String = description(sender) if InStrB(name,"NSSearchField")>0 then // サーチフィールド Declare Function stringValue Lib "Cocoa" Selector "stringValue" (receiver As Ptr) As CFStringRef messageBox stringValue(sender) AddAutoComp(stringValue(sender)) // オートコンプリートリスト登録 elseif InStrB(name,"NSSegmentedControl")>0 then // セグメンテッドコントロール Declare Function selectedSegment Lib "Cocoa" Selector "selectedSegment" (receiver As Ptr) As Integer Declare Function cell Lib "Cocoa" Selector "cell" (receiver As Ptr) As Ptr Declare Function tagForSegment Lib "Cocoa" Selector "tagForSegment:" (receiver As Ptr, seg As Integer) As Integer Dim tagno As Integer = tagForSegment(cell(sender), selectedSegment(sender)) if tagno >= 600 and tagno < 700 then // 1セグメントコントロール(通常のアイテム代わり) Declare Sub setSelected Lib "Cocoa" Selector "setSelected:forSegment:" (receiver As Ptr, selected As Boolean, no As Integer) setSelected(sender, false, 0) // ボタンを押下状態から戻す select case tagno case 601 // 新規 messageBox "新規" case 602 // 保存 messageBox "保存" end select end if else messageBox "No Define" end if End Sub
- 以下をCocoaToolBarにペースト
Private Shared Function controlTextViewCompletions(id As Ptr, SEL As CString, control As Ptr, textView As Ptr, words As Ptr, charRange As NSRange, index As Integer) As Ptr // オートコンプリートのモードによって、返す配列を変える if gCustomComp then // Custom // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Dim arr As Ptr = NSClassFromString("NSMutableArray") // クラスメソッドなので、まずNSMutableArrayクラスを取得 Declare Function getArray Lib "Cocoa" Selector "array" (receiver As Ptr) As Ptr // Return Array* arr=getArray(arr) // gAutoCompからArrayを生成 Declare Sub addObject Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As CFStringRef) for i As Integer = 0 to gAutoComp.Ubound addObject(arr, gAutoComp(i)) next return arr else // Default return words end if End Function
- CocoaToolBarのcontrolTextViewDoCommandBySelectorメソッドを以下に差し替え
Private Shared Function controlTextViewDoCommandBySelector(id As Ptr, sel As Ptr, control As Ptr, textView As Ptr, command As Ptr) As Boolean Dim mb As MemoryBlock mb = command if mb.CString(0) = "insertNewline:" then // retunキーが押された // インスタンス側でActionを受け取るメソッドをレイズする ActionHandler.Invoke(control) return true // 自前の処理を実行して終了 else return false // システムに処理を任せる end if End Function
- CocoaToolBarのmakeDelegateTextViewメソッドを以下に差し替え
Private Shared Function makeDelegateTextView(name As String) As Ptr // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 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 SearchDelegateInstance <> nil then return SearchDelegateInstance end if // クラス名を引数のname(名前は任意だが、重複を避けるためにアイテムのIdentifierにしている。)、メタクラス名をNSObjectにして、生成 Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), name, 0) // ランタイムに登録(参照を可能とするため) objc_registerClassPair newClassId // Delegateの対象となるメソッドを追加(control:textView:doCommandBySelector:をXojo側で用意したcontrolTextViewDoCommandBySelectorメソッドで受け取る。) if not class_addMethod (newClassId, NSSelectorFromString("control:textView:doCommandBySelector:"), AddressOf controlTextViewDoCommandBySelector, "v@:@@@") then msgBox "error." return nil end if // Delegateの対象となるメソッドを追加(control:textView:completions:forPartialWordRange:indexOfSelectedItem:をXojo側で用意したcontrolTextViewCompletionsメソッドで受け取る。) if not class_addMethod (newClassId, NSSelectorFromString("control:textView:completions:forPartialWordRange:indexOfSelectedItem:"), AddressOf controlTextViewCompletions, "v@:@@@@@") then msgBox "error11." return nil 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 delegateId As Ptr = init(alloc(newClassId)) // インスタンスを保持 SearchDelegateInstance = delegateId // インスタンスを返す return delegateId End Function
- 新規モジュールを作成(名前は、ここでは「AutoComplete」とした。)
- 以下をAutoCompleteにペースト(できなければPropertyに、Name:gAutoComp(-1)、Type:String、を追加)
Public Property gAutoComp(-1) As String
- 以下をAutoCompleteにペースト(できなければPropertyに、Name:gCustomComp、Type:Boolean、を追加)
Public Property gCustomComp As Boolean
おわりに
上記例では単語レベルですが、文も登録できるので、定型文をワンタッチで呼び出す、といった使い道も考えられるでしょう。
また、例1ではボタンにショートカットを設定しましたが、素直にメニューアイテムで実装した方が融通が効くかも。
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):Macのテキストエディットで単語補完を表示する - Apple サポート (日本)
更新履歴
2024.09.10 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]