ホームページ開発ツール>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キーと表記)を押すと候補が表示され、続けてリターン>スペースを繰り返していくと、何やら文が出来上がっていきます。
S Shot1

 また、途中まで打ってF5キーを押すと、それに続く候補が表示されます。
S Shot2

 参考サイト(1):Macのテキストエディットで単語補完を表示する - Apple サポート (日本)

 ただし、これは英語のみで、日本語が含まれると全く表示されなくなります。
 では、同等の機能を日本語でも、となると、これは一筋縄ではいかないことは容易に想像できます。(簡単なら、とっくに標準実装されているだろうし。)
 ここでは、仕組みはそのまま利用して、あらかじめ登録しておいた候補を表示する、ことを目指します。

 候補リストは、NSTextView(DesktopTextAreaの親クラス。以下、前者)とNSTextField(DesktopTextFieldの親クラス。以下、後者)で異なり、前者はNSTextViewDelegatetextView:completions:forPartialWordRange:indexOfSelectedItem:メソッド、後者はNSControlTextEditingDelegatecontrol:textView:completions:forPartialWordRange:indexOfSelectedItem:メソッドで返します。
 これで、F5キーを押した時のリストが入れ替わります。(デフォルトのリストにしたい場合は、引数で渡ってきたリストをそのまま返します。)

 今回は、この両者を試してみることにします。
 前者はDesktopTextAreaとしますが、後者はNSTextFieldを継承したCocoaツールバーの検索フィールド(NSSearchField)を対象とします。

 DesktopTextAreaには既にDelegateが設定済なので、置き換えてしまうと副作用が生じるため、以前にもやった既存のDelegateをカテゴリー拡張する方法で対処します。この場合、拡張は全てのDesktopTextAreaに適用されますが、それはそれでよしとします。(実験目的なので)
 検索フィールドの方は既存のDelegateがありませんので、通常のランタイムAPIのインスタンスを用います。

 候補リストは、あらかじめ作成しておいて常に同じものを表示する、としてもいいのですが、ここではその場で登録することも考えてみます。
 登録のタイミングは、前者は文字列選択後にボタンを押すという明示的なものとし、後者はリターンキー押下時とします。

 以上を踏まえ、(残りの)仕様は以下の通りとしました。

 例1(DesktopTextArea)
 例2(検索フィールド)
 共通

 Xojoでの実装(例1)
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. Xojoで新規プロジェクトを作成
  2. Window1に、DesktopButton(Name:Button1, Caption:登録 ⌘E)、DesktopCheckBox(Name:CheckBox1, Caption:カスタムオートコンプリート)、DesktopTextArea(Name:TextArea1)を置く
  3. 以下を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
    
  4. 以下をButton1にペースト(できなければ、Sub - Endの間をPressedイベントに記述)
    Sub Pressed() Handles Pressed
      // 指定された文字列をリストに追加
      AddAutoComp(TextArea1.SelectedText)
    End Sub
    
  5. 以下をCheckBox1にペースト(できなければ、Sub - Endの間をValueChangedイベントに記述)
    Sub ValueChanged() Handles ValueChanged
      gCustomComp=me.Value
    End Sub
    
  6. 以下をWindow1にペースト(できなければ、Sub - Endの間をClosingイベントに記述)
    Sub Closing() Handles Closing
      // カスタムオートコンプリート用リスト書き出し
      SaveAutoComp()
    End Sub
    
  7. 以下を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
    
  8. 以下を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
    
  9. 以下を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
    
  10. 以下を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
    
  11. 新規クラスを作成(名前は、ここでは「NSTextViewDelegate」とした。)
  12. 以下を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
    
  13. 以下をNSTextViewDelegateにペースト
    Public Sub Constructor(textView As Ptr)
      // Delegate設定
      RegisterMethod()
      
      // レイズされたクラスメソッドをインスタンス側で処理するための仕込み
      ActionHandler = AddressOf MakeCompList
    End Sub
    
  14. 以下を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
    
  15. 以下を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
    
  16. 以下を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
    
  17. 以下をNSTextViewDelegateにペースト(できなければShared Propertyに、Name:ActionHandler、Type:ActionDelegate、を追加)
    Private Shared Property ActionHandler As ActionDelegate
    
  18. 新規モジュールを作成(名前は、ここでは「AutoComplete」とした。)
  19. 以下をAutoCompleteにペースト(できなければPropertyに、Name:gAutoComp(-1)、Type:String、を追加)
    Public Property gAutoComp(-1) As String
    
  20. 以下をAutoCompleteにペースト(できなければPropertyに、Name:gCustomComp、Type:Boolean、を追加)
    Public Property gCustomComp As Boolean
    
  21. 他に、NSRange(構造体)が必要ですが、それらはmacoslibからコピーさせて頂きました。(別途モジュールを用意してコピーする。)
    注)macoslibではメソッドの引数、構造体のメンバーの型に、Singleが割り当てられているものがあるが、それらはCGFloatに書き換える。
 実行してみたところ、カスタムオートコンプリートが機能することを確認しました。
S Shot3

 Xojoでの実装(例2)
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. CocoaのDeclareでツールバーを実装する・Big Surの新機能を試すのプロジェクトをベースとする
    (注:このプロジェクトはXojo 2021 Release 1で作成したものであり、Xojo 2021 Release 3以降のAPIとは異なります。Xojo 2021 Release 3以降で新規に作成する場合は、イベント名等が異なりますので、例1を参考にする等して対応して下さい。)
  2. Window1に、CheckBox(Name:CheckBox1, Caption:カスタムオートコンプリート)を追加
  3. 以下をCheckBox1にペースト
    Sub Action() Handles Action
      gCustomComp=me.Value
    End Sub
    
  4. 以下をWindow1にペースト
    Sub Close() Handles Close
      // カスタムオートコンプリート用リスト書き出し
      SaveAutoComp()
    End Sub
    
  5. Window1のOpenイベントを以下に差し替え
    Sub Open() Handles Open
      // SplitViewとToolbar生成
      InitToolbar(nil)  // 今回はSplitViewは使わないので、nilを渡す
      
      // カスタムオートコンプリート用リスト読み込み
      LoadAutoComp()
    End Sub
    
  6. 以下を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
    
  7. 以下を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
    
  8. 以下を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
    
  9. 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
    
  10. 以下を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
    
  11. 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
    
  12. 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
    
  13. 新規モジュールを作成(名前は、ここでは「AutoComplete」とした。)
  14. 以下をAutoCompleteにペースト(できなければPropertyに、Name:gAutoComp(-1)、Type:String、を追加)
    Public Property gAutoComp(-1) As String
    
  15. 以下をAutoCompleteにペースト(できなければPropertyに、Name:gCustomComp、Type:Boolean、を追加)
    Public Property gCustomComp As Boolean
    
 実行してみたところ、カスタムオートコンプリートが機能することを確認しました。
S Shot4

 おわりに

 上記例では単語レベルですが、文も登録できるので、定型文をワンタッチで呼び出す、といった使い道も考えられるでしょう。

 また、例1ではボタンにショートカットを設定しましたが、素直にメニューアイテムで実装した方が融通が効くかも。


 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):Macのテキストエディットで単語補完を表示する - Apple サポート (日本)


 更新履歴

 2024.09.10 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]