from typing import List, Dict import gi gi.require_version('IBus', '1.0') from gi.repository import IBus from gi.repository import GLib import os import sys import re import logging import pathlib from jaconv import jaconv from comb import combromkan from comb.engine import Comb from comb.node import Node from comb.user_language_model import UserLanguageModel from comb.system_dict import SystemDict from comb.user_dict import load_user_dict_from_json_config MODE_KANA = 1 MODE_ALPHA = 2 num_keys = [] for n in range(1, 10): num_keys.append(getattr(IBus, str(n))) num_keys.append(getattr(IBus, '0')) del n numpad_keys = [] for n in range(1, 10): numpad_keys.append(getattr(IBus, 'KP_' + str(n))) numpad_keys.append(getattr(IBus, 'KP_0')) del n configdir = os.path.join(GLib.get_user_config_dir(), 'ibus-comb') pathlib.Path(os.path.join(configdir, 'user-dict')).mkdir(parents=True, exist_ok=True) logging.info(f"Loading user dictionary: {configdir}") user_language_model = UserLanguageModel(os.path.join(configdir, 'user-dict')) logging.info("Loaded user dictionary") system_dict = SystemDict.create() logging.info("Loaded system dictionary") try: user_dict_conf_path = os.path.join(configdir, 'user-dict.json') logging.info(f"user_dict_conf_path={user_dict_conf_path}") user_dict = None if os.path.exists(user_dict_conf_path): logging.info(f"Loading '{user_dict_conf_path}'") try: user_dict = load_user_dict_from_json_config(user_dict_conf_path) except: logging.error("Cannot load user dictionary", exc_info=True) else: logging.info(f"'{user_dict_conf_path}' does not exist.") comb = Comb(user_language_model, system_dict, user_dict) logging.info("Finished Comb.") except: logging.error("Cannot initialize.", exc_info=True) sys.exit(1) # ---------------------------------------------------------------------- # the engine # ---------------------------------------------------------------------- class CombIBusEngine(IBus.Engine): user_language_model: UserLanguageModel current_clause: int node_selected: Dict[int, int] clauses: List[List[Node]] prop_list: IBus.PropList comb: Comb mode: int force_selected_clause: List[slice] __gtype_name__ = 'CombIBusEngine' def __init__(self): super(CombIBusEngine, self).__init__() self.is_invalidate = False # 未確定文字列。 self.preedit_string = '' # 候補文字列 self.lookup_table = IBus.LookupTable.new(page_size=10, cursor_pos=0, cursor_visible=True, round=True) self.prop_list = IBus.PropList() self.comb = comb self.user_language_model = user_language_model self.user_dict = user_dict self.logger = logging.getLogger(__name__) self.mode = MODE_KANA # 変換候補。文節ごとの配列。 self.clauses = [] # 現在選択されている、文節。 self.current_clause = 0 # key は、clause 番号。value は、node の index。 self.node_selected = {} # 文節を選びなおしたもの。 self.force_selected_clause = [] # カーソル変更をしたばっかりかどうかを、みるフラグ。 self.cursor_moved = False self.logger.debug("Create Comb engine OK") def set_lookup_table_cursor_pos_in_current_page(self, index): """Sets the cursor in the lookup table to index in the current page Returns True if successful, False if not. """ self.logger.info(f"set_lookup_table_cursor_pos_in_current_page: {index}") page_size = self.lookup_table.get_page_size() if index > page_size: self.logger.info(f"index too big: {index} > {page_size}") return False page, pos_in_page = divmod(self.lookup_table.get_cursor_pos(), page_size) new_pos = page * page_size + index if new_pos > self.lookup_table.get_number_of_candidates(): self.logger.info(f"new_pos too big: {new_pos} > {self.lookup_table.get_number_of_candidates()}") return False self.lookup_table.set_cursor_pos(new_pos) self.node_selected[self.current_clause] = self.lookup_table.get_cursor_pos() return True def do_candidate_clicked(self, index, dummy_button, dummy_state): if self.set_lookup_table_cursor_pos_in_current_page(index): self.commit_candidate() def do_process_key_event(self, keyval, keycode, state): try: return self._do_process_key_event(keyval, keycode, state) except: self.logger.error(f"Cannot process key event: keyval={keyval} keycode={keycode} state={state}", exc_info=True) return False def _do_process_key_event(self, keyval, keycode, state): # self.logger.debug("process_key_event(%04x, %04x, %04x)" % (keyval, keycode, state)) # ignore key release events is_press = ((state & IBus.ModifierType.RELEASE_MASK) == 0) if not is_press: return False # 入力モードの切り替え機能。 if keyval == IBus.Henkan: self.logger.info("Switch to kana mode") self.mode = MODE_KANA return True elif keyval == IBus.Muhenkan: self.logger.info("Switch to alpha mode") self.mode = MODE_ALPHA return True if self.preedit_string: if keyval in (IBus.Return, IBus.KP_Enter): if self.in_henkan_mode(): self.commit_candidate() else: # 無変換状態では、ひらがなに変換してコミットします。 self.commit_string(combromkan.to_hiragana(self.preedit_string)) return True elif keyval == IBus.Escape: self.preedit_string = '' self.update_candidates() return True elif keyval == IBus.BackSpace: if self.in_henkan_mode(): # 変換中の場合、無変換モードにもどす。 self.lookup_table.clear() self.hide_auxiliary_text() self.hide_lookup_table() else: # サイゴの一文字をけずるが、子音が先行しているばあいは、子音もついでにとる。 self.preedit_string = re.sub('(?:z[hjkl.-]|n+|[kstnhmyrwgzjdbp]?[aiueo]|.)$', '', self.preedit_string) # 変換していないときのレンダリングをする。 self.update_preedit_text_before_henkan() return True elif keyval in num_keys and self.in_henkan_mode(): # TODO: 変換候補が表示されている状態の時にのみハンドリングされるべき。 index = num_keys.index(keyval) if self.set_lookup_table_cursor_pos_in_current_page(index): self.refresh() return True return False elif keyval in numpad_keys and self.in_henkan_mode(): # TODO: 変換候補が表示されている状態の時にのみハンドリングされるべき。 index = numpad_keys.index(keyval) if self.set_lookup_table_cursor_pos_in_current_page(index): self.refresh() return True return False elif keyval in (IBus.Page_Up, IBus.KP_Page_Up): self.page_up() return True elif keyval in (IBus.Page_Down, IBus.KP_Page_Down): self.page_down() return True elif keyval in (IBus.Up, IBus.KP_Up): self.cursor_up() return True elif keyval in (IBus.Down, IBus.KP_Down): self.cursor_down() return True elif keyval in (IBus.Right, IBus.KP_Right): if state & IBus.ModifierType.SHIFT_MASK == 0: self.cursor_right() else: self.extend_clause_right() return True elif keyval in (IBus.Left, IBus.KP_Left): if state & IBus.ModifierType.SHIFT_MASK == 0: self.cursor_left() else: self.extend_clause_left() return True elif keyval == IBus.F6: # F6 convert selected word/characters to full-width hiragana (standard hiragana): ホワイト → ほわいと self.convert_to_full_hiragana() return True elif keyval == IBus.F7: # F7 convert to full-width katakana (standard katakana): ほわいと → ホワイト self.convert_to_full_katakana() return True elif keyval == IBus.F8: # F8 convert to half-width katakana (katakana for specific purpose): ホワイト → ホワイト self.convert_to_half_katakana() return True elif keyval == IBus.F9: # F9 convert to full-width romaji, all-capitals, proper noun capitalization (latin script inside # Japanese text): ホワイト → howaito → HOWAITO → Howaito self.convert_to_full_romaji() return True elif keyval == IBus.F10: # F10 convert to half-width romaji, all-capitals, proper noun capitalization (latin script like # standard English): ホワイト → howaito → HOWAITO → Howaito self.convert_to_half_romaji() return True # スペース if keyval == IBus.space: if len(self.preedit_string) == 0: # もし、まだなにもはいっていなければ、ただの空白をそのままいれる。 return False else: if self.in_henkan_mode(): self.logger.debug("cursor down.") self.cursor_down() else: # 実際に変換していく。 self.logger.debug("update_candidates.") self.update_candidates() return True if self.mode == MODE_KANA: # Allow typing all ASCII letters and punctuation if ord('!') <= keyval <= ord('~'): if state & (IBus.ModifierType.CONTROL_MASK | IBus.ModifierType.MOD1_MASK) == 0: if self.in_henkan_mode(): self.commit_candidate() self.preedit_string += chr(keyval) # この時点では、preedit string にだけ、追加して表示するひつようがあります。 self.update_preedit_text_before_henkan() return True else: if keyval < 128 and self.preedit_string: self.commit_string(self.preedit_string) else: return False return False def in_henkan_mode(self): return self.lookup_table.get_number_of_candidates() > 0 def convert_to_full_katakana(self): self.logger.info("Convert to full katakana") # カタカナ候補のみを表示するようにする。 hira = combromkan.to_hiragana(self.preedit_string) kata = jaconv.hira2kata(hira) self.convert_to_single(hira, kata) def convert_to_full_hiragana(self): self.logger.info("Convert to full hiragana") # カタカナ候補のみを表示するようにする。 hira = combromkan.to_hiragana(self.preedit_string) self.convert_to_single(hira, hira) def convert_to_half_katakana(self): self.logger.info("Convert to half katakana") # 半角カタカナ候補のみを表示するようにする。 hira = combromkan.to_hiragana(self.preedit_string) kata = jaconv.hira2kata(hira) kata = jaconv.z2h(kata) self.convert_to_single(hira, kata) def convert_to_half_romaji(self): self.logger.info("Convert to half romaji") # 半角カタカナ候補のみを表示するようにする。 hira = combromkan.to_hiragana(self.preedit_string) romaji = jaconv.z2h(self.preedit_string) self.convert_to_single(hira, romaji) def convert_to_full_romaji(self): self.logger.info("Convert to full romaji") hira = combromkan.to_hiragana(self.preedit_string) romaji = jaconv.h2z(self.preedit_string, kana=True, digit=True, ascii=True) self.convert_to_single(hira, romaji) def convert_to_single(self, yomi, word) -> None: """ 特定の1文節の文章を候補として表示する。 F6 などを押した時用。 """ # 候補を設定 self.clauses = [[Node(start_pos=0, word=word, yomi=yomi)]] self.current_clause = 0 self.node_selected = {} self.force_selected_clause = [] # ルックアップテーブルに候補を設定 self.lookup_table.clear() candidate = IBus.Text.new_from_string(word) self.lookup_table.append_candidate(candidate) # 表示を更新 self.refresh() def invalidate(self): if self.is_invalidate: return self.is_invalidate = True GLib.idle_add(self.update_candidates) def page_up(self): if self.lookup_table.page_up(): self.node_selected[self.current_clause] = self.lookup_table.get_cursor_pos() self.cursor_moved = True self.refresh() return True return False def page_down(self): if self.lookup_table.page_down(): self.node_selected[self.current_clause] = self.lookup_table.get_cursor_pos() self.cursor_moved = True self.refresh() return True return False def cursor_up(self): if self.lookup_table.cursor_up(): self.node_selected[self.current_clause] = self.lookup_table.get_cursor_pos() self.cursor_moved = True self.refresh() return True return False def cursor_down(self): """ 次の変換候補を選択する。 """ if self.lookup_table.cursor_down(): self.node_selected[self.current_clause] = self.lookup_table.get_cursor_pos() self.cursor_moved = True self.refresh() return True return False # 選択する分節を右にずらす。 def cursor_right(self): """ 選択する分節を右にずらす。 """ self.logger.info(f"right cursor") # いっこしか分節がない場合は、何もせぬ if len(self.clauses) == 0: self.logger.info(f"right cursor:no clauses") return False # 既に一番右だった場合、一番左にいく。 if self.current_clause == len(self.clauses) - 1: self.current_clause = 0 else: self.current_clause += 1 self.cursor_moved = True self.create_lookup_table() self.refresh() # 選択する分節を左にずらす。 def cursor_left(self): self.logger.info(f"left cursor") # いっこしか分節がない場合は、何もせぬ if len(self.clauses) == 0: self.logger.info(f"left cursor:no clauses") return False # 既に一番左だった場合、一番右にいく。 if self.current_clause == 0: self.current_clause = len(self.clauses) - 1 else: self.current_clause -= 1 self.cursor_moved = True self.create_lookup_table() self.refresh() def extend_clause_right(self): """ 文節の選択範囲を広げることを支持する """ if len(self.clauses) == 0: return False max_len = max([clause[0].start_pos + len(clause[0].yomi) for clause in self.clauses]) self.force_selected_clause = [] for i, clause in enumerate(self.clauses): node = clause[0] if self.current_clause == i: # 現在選択中の文節の場合、伸ばす。 self.force_selected_clause.append( slice(node.start_pos, min(node.start_pos + len(node.yomi) + 1, max_len))) elif self.current_clause + 1 == i: # 次の分節を一文字ヘラス self.force_selected_clause.append( slice(node.start_pos + 1, node.start_pos + len(node.yomi))) else: # それ以外は現在指定の分節のまま self.force_selected_clause.append( slice(node.start_pos, node.start_pos + len(node.yomi))) self.force_selected_clause = [x for x in self.force_selected_clause if x.start != x.stop] self._update_candidates() def extend_clause_left(self): """ 文節の選択範囲を広げることを支持する """ if len(self.clauses) == 0: return False # 一番左の文節にフォーカスがある場合、一番左の文節が短くなるべき。 target_clause = 1 if self.current_clause == 0 and len(self.clauses) > 1 else self.current_clause self.force_selected_clause = [] for i, clause in enumerate(self.clauses): node = clause[0] if target_clause == i: # 現在選択中の文節の場合、伸ばす。 self.force_selected_clause.append( slice(node.start_pos - 1, node.start_pos + len(node.yomi))) elif target_clause - 1 == i: # 前の分節を一文字ヘラス self.force_selected_clause.append( slice(node.start_pos, node.start_pos + len(node.yomi) - 1)) else: # それ以外は現在指定の分節のまま self.force_selected_clause.append( slice(node.start_pos, node.start_pos + len(node.yomi))) self.force_selected_clause = [x for x in self.force_selected_clause if x.start != x.stop] self._update_candidates() def commit_string(self, text): self.cursor_moved = False if self.in_henkan_mode(): # 変換モードのときのみ学習を実施する。 candidate_nodes = [] for clauseid, nodes in enumerate(self.clauses): candidate_nodes.append(nodes[self.node_selected.get(clauseid, 0)]) self.user_language_model.add_entry(candidate_nodes) # user language model の書き出しは、バックグラウンドスレッドでやりたい。 self.user_language_model.save() self.commit_text(IBus.Text.new_from_string(text)) self.preedit_string = '' self.clauses = [] self.current_clause = 0 self.node_selected = {} self.force_selected_clause = [] self.update_candidates() def build_string(self): result = '' for clauseid, nodes in enumerate(self.clauses): result += nodes[self.node_selected.get(clauseid, 0)].word return result def commit_candidate(self): s = self.build_string() self.logger.info(f"Committing {s}") self.commit_string(s) def update_candidates(self): self.logger.info("update_candidates") try: self._update_candidates() except: self.logger.error(f"cannot get kanji candidates {sys.exc_info()[0]}", exc_info=True) def _update_candidates(self): if len(self.preedit_string) > 0: # 変換をかける self.clauses = self.comb.convert(self.preedit_string, self.force_selected_clause) else: self.clauses = [] self.create_lookup_table() self.current_clause = 0 self.node_selected = {} self.refresh() def refresh(self): preedit_len = len(self.preedit_string) if len(self.clauses) == 0: self.hide_auxiliary_text() self.hide_lookup_table() self.hide_preedit_text() return current_clause = self.clauses[self.current_clause] current_node = current_clause[0] # -- auxiliary text(ポップアップしてるやつのほう) first_candidate = current_node.yomi auxiliary_text = IBus.Text.new_from_string(first_candidate) auxiliary_text.set_attributes(IBus.AttrList()) self.update_auxiliary_text(auxiliary_text, preedit_len > 0) text = self.build_string() preedit_attrs = IBus.AttrList() # 全部に下線をひく。 preedit_attrs.append(IBus.Attribute.new(IBus.AttrType.UNDERLINE, IBus.AttrUnderline.SINGLE, 0, len(text))) bgstart = sum([len(self.clauses[i][0].word) for i in range(0, self.current_clause)]) # 背景色を設定する。 preedit_attrs.append(IBus.Attribute.new( IBus.AttrType.BACKGROUND, 0x00333333, bgstart, bgstart + len(current_node.word))) preedit_text = IBus.Text.new_from_string(text) preedit_text.set_attributes(preedit_attrs) self.update_preedit_text(preedit_text, len(text), len(text) > 0) # 候補があれば、選択肢を表示させる。 self._update_lookup_table() self.is_invalidate = False def update_preedit_text_before_henkan(self): """ 無変換状態で、どんどん入力していくフェーズ。 """ if len(self.preedit_string) == 0: self.hide_preedit_text() return # 平仮名にする。 text = combromkan.to_hiragana(self.preedit_string) self.clauses = [ [Node(word=text, yomi=text, start_pos=3)] ] self.current_clause = 0 preedit_attrs = IBus.AttrList() preedit_attrs.append(IBus.Attribute.new(IBus.AttrType.UNDERLINE, IBus.AttrUnderline.SINGLE, 0, len(text))) preedit_text = IBus.Text.new_from_string(text) preedit_text.set_attributes(preedit_attrs) self.update_preedit_text(preedit_text, len(text), len(text) > 0) def create_lookup_table(self): """ 現在の候補選択状態から、 lookup table を構築する。 """ # 一旦、ルックアップテーブルをクリアする self.lookup_table.clear() # 現在の未変換情報を元に、候補を産出していく。 if len(self.clauses) > 0: # lookup table に候補を詰め込んでいく。 for node in self.clauses[self.current_clause]: candidate = IBus.Text.new_from_string(node.word) self.lookup_table.append_candidate(candidate) def _update_lookup_table(self): """ 候補があれば lookup table を表示。なければ非表示にする。 """ visible = self.lookup_table.get_number_of_candidates() > 0 self.update_lookup_table(self.lookup_table, visible) def do_focus_in(self): # self.logger.debug("focus_in") self.register_properties(self.prop_list) def do_focus_out(self): # self.logger.debug("focus_out") self.do_reset() def do_reset(self): # self.logger.debug("reset") self.preedit_string = '' self.force_selected_clause = [] self.clauses = [] self.current_clause = 0 self.lookup_table.clear() self.hide_auxiliary_text() self.hide_lookup_table() def do_property_activate(self, prop_name): self.logger.debug("PropertyActivate(%s)" % prop_name) def do_page_up(self): return self.page_up() def do_page_down(self): return self.page_down() def do_cursor_up(self): return self.cursor_up() def do_cursor_down(self): return self.cursor_down()