Files
akaza/comb/ui.py
Tokuhiro Matsuno 775f14ddff support user dict.
2020-09-13 15:49:35 +09:00

657 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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): ホワイト →
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 cursorno 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 cursorno 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()