テキスト編集その2の続き。今回は IME 入力に関して。だいぶ苦労した。。。。

NSTextInputClient の実装。動作を見つつ以下の感じで落ち着いた。

まず、最低限必要な変数。

 

    var isStrIME: Bool = false;
    var strIME: NSAttributedString = NSAttributedString();
    var selectedIMERange : NSRange = NSMakeRange(NSNotFound, 0);

 

以下の markedText は、変換中の文字列。

 

isStrIME は、markedText が存在するかどうか。strIME に markedText は格納するのだけど、

これは再変換時に再利用することがあるため別途フラグをもつことに。

 

strIME は、markedText をテキスト修飾した文字列を格納。

 

selectedIMERange は、再変換時に再変換の対象とする文字列内の範囲。

 

次に最低限必要な protocol の実装。

 

insertText

    func insertText(_ string: Any, replacementRange: NSRange){
        self.unmarkText();
        if ((replacementRange.location == NSNotFound) || (replacementRange.length == 0)){
            editWin.insert_string(string as? String);
        } else {
            editWin.replace_string(string as? String);
        }
        self.selectedIMERange = NSMakeRange(NSNotFound, 0);
    }

確定した文字列がセットされて呼び出される。

再変換かどうかは、selectedIMERange がセットされているかどうかで判断して問題ないみたい。

再変換に備えて挿入した位置と文字数は保存しておかないと駄目だと思う。

 

setMarkedText

    func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange){
        self.selectedIMERange = selectedRange;
        let str = string as? NSAttributedString;
        if (str != nil){
            self.isStrIME = true;
            self.strIME = string as! NSAttributedString;
            editWin.setStrIME(strIME);
        } else {
            unmarkText();
        }
    }

入力された変換中の文字列がセットされて呼び出される。

後述のunmarkText とセットにして使い、変換中文字列の画面への表示を行えばいい。

 

unmarkText

    func unmarkText(){
        self.isStrIME = false;
        editWin.resetStrIME();
    }

変換中の文字列のクリア。ここでフラグと表示をクリアする。

 

markedRange

    func markedRange() -> NSRange {
        if (!self.isStrIME){
            return NSMakeRange(NSNotFound, 0);
        }
        return NSMakeRange(0, strIME.string.count);
    }

変換中文字列の範囲。セットされた文字列全域を返すで良いんじゃないかな。

 

hasMarkedText

    func hasMarkedText() -> Bool {
        return         return self.isStrIME;
    }

 変換中の文字列が存在するかどうかを返す。

 

 validAttributesForMarkedText

    func validAttributesForMarkedText() -> [NSAttributedString.Key] {
        return [ NSAttributedString.Key.underlineColor, NSAttributedString.Key.underlineStyle, NSAttributedString.Key.markedClauseSegment ];
    }

変換中文字列に有効なテキスト修飾を返す。これ効いてんのかな、、一応返してるけどね。

 

firstRect

    func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
        var rc = editWin.getCurrentCursorRect();
        rc = self.convert(rc, to: nil);
        rc = self.window!.convertToScreen(rc);
        return rc;
    }

 変換中の文字列を表示している座標をスクリーン座標で返す。現在のカーソル位置で良いね。

 

characterIndex

    func characterIndex(for point: NSPoint) -> Int {
        return NSNotFound;
    }

 画面座標から文字位置を返す。指定位置での再変換とかで利用するのかな?今は常に NSNotFound 返してる。

 

doCommand

    override func doCommand(by selector: Selector){
        if responds(to: selector){
            perform(selector, with: nil, afterDelay: 0.1)
        }
    }

 カーソル移動などのキー入力がセレクタとして渡されるので、実装されていたら呼び出し。

cancel とか moveUp、scrollPageUp などなど。

 

 

ここからは再変換に関する部分。

 

selectedRange 

    func selectedRange() -> NSRange {
        if ((self.strIME.string.count == 0) || (self.selectedIMERange.location == NSNotFound) || (self.selectedIMERange.length == 0)){
            return NSMakeRange(NSNotFound, 0);
        }
        return self.selectedIMERange;
    }

 再変換対象の文字列のうち、どの部分を対象とするか。

 

attributedSubstring

    func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
        return nil;
    }

 なんとなく想像できるけど、今は nil を返してる。

 

attributedString

    func attributedString() -> NSAttributedString {
        if (self.strIME.string.count == 0){
                return NSAttributedString(string: "");
        }
        self.selectedIMERange = NSMakeRange(0, self.strIME.string.count);
        return self.strIME;
    }

 これ optional なんだけど、再変換を実装するなら必須。

再変換時に変換中文字列としてセットしたい文字列を返す。今は前回の入力文字列を返すようにしてる。

 

ふーっ。ここまでで protocol 実装は終わり。

その他に、

 keyDown

    override func keyDown(with event: NSEvent){
        if (inputContext?.handleEvent(event) != true){
                super.keyDown(with:event);
        }
    }

keyDown の中で、inputContext.handleEvent を呼ぶ。

 

cancelOperation

    @objc override func cancelOperation(_ sender : Any?){
         unmarkText();
    }

ESC を二回押下すると呼ばれる。変換中文字列をクリアされたと判断。

 

 

その他に、これは好みだと思うけど変換中にはマウスドラッグでカーソルを移動されたくないので、

override func mouseDragged(with event: NSEvent) {
        if ((self.markedRange().location == NSNotFound) || (self.markedRange().length == 0)){
          // マウスドラッグの実装

 このように、変換中文字列がない場合のみマウスドラッグでの処理を許すように変更。

 

あとは変換中の文字列の実際の表示だけ。これはまぁ存在する場合はそれを挿入表示するようにしてやればオッケー。

 attributedString を追っかけてなかったおかげで再変換の流れが分からずに、えらい時間かかっちまっただ。。

 

テキスト編集その1からの続き。

編集することによって影響を受けるものの一つ、行の管理について。

行は今まで行管理クラスに、実際のテキストが格納されている位置を保存してた。

 

class CTextLine {
		bsize_t m_start;		///< 行先端のテキストバッファ位置
		bsize_t m_end;			///< 行終端のテキストバッファ位置

 

これだと文字が挿入/削除された際にずれてしまう。したがってずれた分補正をしてやる必要があるのだけれど、

これを影響の受ける行全てに対して行っていくと、行数が多い場合に大変な時間がかかってしまう。

なので以下のような構造体を定義。

 

typedef struct _text_line_adjust_t {

	bsize_t line;			///< 影響を受ける開始行
	bsize_t add;			///< 補正値
} text_line_adjust_t;

 

この構造体は、変更があった行から以降の行がどれくらいずれるかの情報を保存。

三行目に一文字挿入されたら、line = 4, add = 1。五文字挿入されたら、line = 4, add = 5。

adjust1

 

別の行に移動した場合はその行までの間だけ、ずれた値を実際の行クラス(上でいう CTextLine)の位置に反映して、

新add には 旧addの値を加算。

adjust2

これで問題ないはず。実際には反映が必要な行数の少ない方を選んで補正してるけど、基本はこんな感じ。

すっごい行数の多いテキストで一番下と一番上を編集とかやると重いかもしんない。

 

ちなみに補正の反映は、行クラスを取得する部分でやることにした。あまり美しくはないけど一番確実だからね。