class EngineHiragana(IBus.Engine): __gtype_name__ = 'EngineHiragana' def __init__(self): super().__init__() self._mode = 'A' # _mode must be one of _input_mode_names self._override = False self._layout = dict() self._to_kana = self._handle_default_layout self._preedit_string = '' self._previous_text = '' self._ignore_surrounding_text = False self._lookup_table = IBus.LookupTable.new(10, 0, True, False) self._lookup_table.set_orientation(IBus.Orientation.VERTICAL) self._init_props() self._config = IBus.Bus().get_config() self._config.connect('value-changed', self._config_value_changed_cb) self._logging_level = self._load_logging_level(self._config) self._dict = self._load_dictionary(self._config) self._layout = self._load_layout(self._config) self._delay = self._load_delay(self._config) self._event = Event(self, self._delay, self._layout) self.set_mode(self._load_input_mode(self._config)) self._set_x4063_mode(self._load_x4063_mode(self._config)) self._shrunk = '' self._committed = '' self._acked = True self.connect('set-surrounding-text', self.set_surrounding_text_cb) self.connect('set-cursor-location', self.set_cursor_location_cb) self._about_dialog = None self._setup_pid = 0 def _init_props(self): self._prop_list = IBus.PropList() self._input_mode_prop = IBus.Property( key='InputMode', prop_type=IBus.PropType.NORMAL, symbol=IBus.Text.new_from_string(self._mode), label=IBus.Text.new_from_string(_("Input mode (%s)") % self._mode), icon=None, tooltip=None, sensitive=False, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None) self._prop_list.append(self._input_mode_prop) prop = IBus.Property(key='Setup', prop_type=IBus.PropType.NORMAL, label=IBus.Text.new_from_string(_("Setup")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None) self._prop_list.append(prop) prop = IBus.Property(key='About', prop_type=IBus.PropType.NORMAL, label=IBus.Text.new_from_string( _("About Hiragana IME...")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None) self._prop_list.append(prop) def _update_input_mode(self): self._input_mode_prop.set_symbol(IBus.Text.new_from_string(self._mode)) self._input_mode_prop.set_label( IBus.Text.new_from_string(_("Input mode (%s)") % self._mode)) self.update_property(self._input_mode_prop) def _load_input_mode(self, config): var = config.get_value('engine/hiragana', 'mode') if var is None or var.get_type_string() != 's' or not var.get_string( ) in INPUT_MODE_NAMES: mode = 'A' if var: config.unset('engine/hiragana', 'mode') else: mode = var.get_string() logger.info("input mode: %s", mode) return mode def _load_logging_level(self, config): var = config.get_value('engine/hiragana', 'logging_level') if var is None or var.get_type_string() != 's' or not var.get_string( ) in NAME_TO_LOGGING_LEVEL: level = 'WARNING' if var: config.unset('engine/hiragana', 'logging_level') else: level = var.get_string() logger.info("logging_level: %s", level) logging.getLogger().setLevel(NAME_TO_LOGGING_LEVEL[level]) return level def _load_dictionary(self, config): var = config.get_value('engine/hiragana', 'dictionary') if var is None or var.get_type_string() != 's': path = os.path.join(package.get_datadir(), 'restrained.dic') if var: config.unset('engine/hiragana', 'dictionary') else: path = var.get_string() return Dictionary(path) def _load_layout(self, config): default_layout = os.path.join(package.get_datadir(), 'layouts') default_layout = os.path.join(default_layout, 'roomazi.json') var = config.get_value('engine/hiragana', 'layout') if var is None: path = default_layout elif var.get_type_string() != 's': config.unset('engine/hiragana', 'layout') path = default_layout else: path = var.get_string() logger.info("layout: %s", path) layout = dict() try: with open(path) as f: layout = json.load(f) except Exception as error: logger.error(error) if not layout: try: with open(default_layout) as f: layout = json.load(f) except Exception as error: logger.error(error) if layout.get('Type') == 'Kana': self._to_kana = self._handle_kana_layout elif 'Roomazi' in layout: self._to_kana = self._handle_roomazi_layout else: self._to_kana = self._handle_default_layout return layout def _load_delay(self, config): var = config.get_value('engine/hiragana', 'delay') if var is None or var.get_type_string() != 'i': delay = 0 if var: config.unset('engine/hiragana', 'delay') else: delay = var.get_int32() logger.info("delay: %d", delay) return delay def _load_x4063_mode(self, config): var = config.get_value('engine/hiragana', 'nn_as_jis_x_4063') if var is None or var.get_type_string() != 'b': mode = True if var: config.unset('engine/hiragana', 'nn_as_jis_x_4063') else: mode = var.get_boolean() logger.info("nn_as_jis_x_4063 mode: {}".format(mode)) return mode def _config_value_changed_cb(self, config, section, name, value): section = section.replace('_', '-') if section != 'engine/hiragana': return logger.debug("config_value_changed({}, {}, {})".format( section, name, value)) if name == "logging_level": self._logging_level = self._load_logging_level(config) elif name == "delay": self._reset() self._delay = self._load_delay(config) self._event = Event(self, self._delay, self._layout) elif name == "layout": self._reset() self._layout = self._load_layout(config) self._event = Event(self, self._delay, self._layout) elif name == "dictionary": self._reset() self._dict = self._load_dictionary(config) elif name == "mode": self.set_mode(self._load_input_mode(self._config)) self._override = True elif name == "nn_as_jis_x_4063": self._set_x4063_mode(self._load_x4063_mode(self._config)) def _handle_default_layout(self, preedit, keyval, state=0, modifiers=0): return self._event.chr(), '' def _handle_kana_layout(self, preedit, keyval, state=0, modifiers=0): yomi = '' c = self._event.chr().lower() if preedit == '\\': preedit = '' if self._event.is_shift(): if 'Shift' in self._layout: yomi = self._layout['\\Shift'][c] elif modifiers & event.SHIFT_L_BIT: yomi = self._layout['\\ShiftL'][c] elif modifiers & event.SHIFT_R_BIT: yomi = self._layout['\\ShiftR'][c] else: yomi = self._layout['\\Normal'][c] else: if self._event.is_shift(): if 'Shift' in self._layout: yomi = self._layout['Shift'][c] elif modifiers & event.SHIFT_L_BIT: yomi = self._layout['ShiftL'][c] elif modifiers & event.SHIFT_R_BIT: yomi = self._layout['ShiftR'][c] else: yomi = self._layout['Normal'][c] if yomi == '\\': preedit += yomi yomi = '' return yomi, preedit def _set_x4063_mode(self, on): if on: self.character_after_n = "aiueo\'wyn" else: self.character_after_n = "aiueo\'wy" logger.debug("set_x4063_mode({})".format(on)) def _handle_roomazi_layout(self, preedit, keyval, state=0, modifiers=0): yomi = '' c = self._event.chr().lower() if preedit == 'n' and self.character_after_n.find(c) < 0: yomi = 'ん' preedit = preedit[1:] preedit += c if preedit in self._layout['Roomazi']: yomi += self._layout['Roomazi'][preedit] preedit = '' if yomi == '\\': preedit = yomi yomi = '' elif 2 <= len( preedit) and preedit[0] == preedit[1] and RE_SOKUON.search( preedit[1]): yomi += 'っ' preedit = preedit[1:] return yomi, preedit def _get_surrounding_text(self): if not (self.client_capabilities & IBus.Capabilite.SURROUNDING_TEXT): self._ignore_surrounding_text = True if self._ignore_surrounding_text or not self._acked: logger.debug("surrounding text: [%s]" % (self._previous_text)) return self._previous_text, len(self._previous_text) tuple = self.get_surrounding_text() text = tuple[0].get_text() pos = tuple[1] # Qt reports pos as if text is in UTF-16 while GTK reports pos in sane manner. # If you're using primarily Qt, use the following code to amend the issue # when a character in Unicode supplementary planes is included in text. # # Deal with surrogate pair manually. (Qt bug?) # for i in range(len(text)): # if pos <= i: # break # if 0x10000 <= ord(text[i]): # pos -= 1 # Qt seems to insert self._preedit_string to the text, while GTK doesn't. # We mimic GTK's behavior here. preedit_len = len(self._preedit_string) if 0 < preedit_len and preedit_len <= pos and text[ pos - preedit_len:pos] == self._preedit_string: text = text[:-preedit_len] pos -= preedit_len logger.debug("surrounding text: '%s', %d, [%s]", text, pos, self._previous_text) return text, pos def _delete_surrounding_text(self, size): self._previous_text = self._previous_text[:-size] if not self._ignore_surrounding_text and self._acked: self.delete_surrounding_text(-size, size) else: # Note a short delay after each BackSpace is necessary for the target application to catch up. for i in range(size): self.forward_key_event(IBus.BackSpace, 14, 0) time.sleep(0.02) self.forward_key_event(IBus.BackSpace, 14, IBus.ModifierType.RELEASE_MASK) def is_overridden(self): return self._override def is_enabled(self): return self.get_mode() != 'A' def enable_ime(self): if not self.is_enabled(): self.set_mode('あ') return True return False def disable_ime(self): if self.is_enabled(): self.set_mode('A') return True return False def switch_zenkaku_hankaku(self): mode = self.get_mode() mode = { 'A': 'A', 'A': 'A', 'ア': 'ア', 'ア': 'ア', 'あ': 'あ' }.get(mode, 'A') return self.set_mode(mode) def switch_katakana(self): mode = self.get_mode() mode = { 'A': 'ア', 'A': 'ア', 'ア': 'あ', 'ア': 'あ', 'あ': 'ア' }.get(mode, 'ア') return self.set_mode(mode) def get_mode(self): return self._mode def set_mode(self, mode): self._override = False if self._mode == mode: return False logger.debug("set_mode(%s)" % (mode)) self._preedit_string = '' self._commit() self._mode = mode self._update() self._update_input_mode() return True def _is_roomazi_mode(self): return self._to_kana == self._handle_roomazi_layout def do_process_key_event(self, keyval, keycode, state): return self._event.process_key_event(keyval, keycode, state) def handle_key_event(self, keyval, keycode, state, modifiers): logger.debug("handle_key_event(%s, %04x, %04x, %04x)" % (IBus.keyval_name(keyval), keycode, state, modifiers)) if self._event.is_dual_role(): pass elif self._event.is_modifier(): # Ignore modifier keys return False elif state & (IBus.ModifierType.CONTROL_MASK | IBus.ModifierType.MOD1_MASK): self._commit() return False # Handle Candidate window if 0 < self._lookup_table.get_number_of_candidates(): if keyval == keysyms.Page_Up or keyval == keysyms.KP_Page_Up: return self.do_page_up() elif keyval == keysyms.Page_Down or keyval == keysyms.KP_Page_Down: return self.do_page_down() elif keyval == keysyms.Up or self._event.is_muhenkan(): return self.do_cursor_up() elif keyval == keysyms.Down or self._event.is_henkan(): return self.do_cursor_down() elif keyval == keysyms.Escape: return self.handle_escape(state) elif keyval == keysyms.Return: self._commit() return True if self._preedit_string: if keyval == keysyms.Return: if self._preedit_string == 'n': self._preedit_string = 'ん' self._commit_string(self._preedit_string) self._preedit_string = '' self._update() return True if keyval == keysyms.Escape: self._preedit_string = '' self._update() return True # Handle Japanese text if self._event.is_henkan(): return self.handle_replace(keyval, state) if self._event.is_shrink(): return self.handle_shrink(keyval, state) self._commit() yomi = '' if self._event.is_katakana(): if self._event.is_shift(): self.switch_katakana() else: self.handle_katakana() return True if self._event.is_backspace(): if 1 <= len(self._preedit_string): self._preedit_string = self._preedit_string[:-1] self._update() return True elif 0 < len(self._previous_text): self._previous_text = self._previous_text[:-1] return False if self._event.is_ascii(): if self.get_mode() == 'A': yomi = to_zenkaku(self._event.chr()) else: yomi, self._preedit_string = self._to_kana( self._preedit_string, keyval, state, modifiers) elif keyval == keysyms.hyphen: yomi = '―' else: self._previous_text = '' return False if yomi: if self.get_mode() == 'ア': yomi = to_katakana(yomi) elif self.get_mode() == 'ア': yomi = to_hankaku(to_katakana(yomi)) self._commit_string(yomi) self._update() return True def lookup_dictionary(self, yomi, pos): if self._preedit_string == 'n': yomi = yomi[:pos] + 'ん' pos += 1 self._lookup_table.clear() cand = self._dict.lookup(yomi, pos) size = len(self._dict.reading()) if 0 < size: if self._preedit_string == 'n': if self._acked: # For furiganapad, yomi has to be committed anyway. # However, 'ん' will be acked by set_cursor_location_cb() # only after the converted text is committed later. # So we pretend that 'ん' is acked here. self._commit_string('ん') self._acked = True self._committed = '' else: size = size - 1 self._preedit_string = '' if 1 < len(self._dict.cand()): for c in self._dict.cand(): self._lookup_table.append_candidate( IBus.Text.new_from_string(c)) return (cand, size) def handle_katakana(self): text, pos = self._get_surrounding_text() for i in reversed(range(pos)): if 0 <= KATAKANA.find(text[i]): continue found = HIRAGANA.find(text[i]) if 0 <= found: self._delete_surrounding_text(pos - i) self._commit_string(KATAKANA[found] + text[i + 1:pos]) break return True def handle_replace(self, keyval, state): if not self._dict.current(): text, pos = self._get_surrounding_text() (cand, size) = self.lookup_dictionary(text, pos) self._shrunk = '' else: size = len(self._shrunk) + len(self._dict.current()) if not self._event.is_shift(): cand = self._dict.next() else: cand = self._dict.previous() if self._dict.current(): self._update() self._delete_surrounding_text(size) self._commit_string(self._shrunk + cand) return True def handle_shrink(self, keyval, state): logger.debug("handle_shrink: '%s'", self._dict.current()) if not self._dict.current(): return False yomi = self._dict.reading() if len(yomi) <= 1: self.handle_escape(state) return True current_size = len(self._shrunk) + len(self._dict.current()) text, pos = self._get_surrounding_text() (cand, size) = self.lookup_dictionary(yomi[1:] + text[pos:], len(yomi) - 1) kana = yomi if 0 < size: kana = kana[:-size] self._shrunk += kana self._delete_surrounding_text(current_size) self._commit_string(self._shrunk + cand) # Update preedit *after* committing the string to append preedit. self._update() return True def handle_escape(self, state): if not self._dict.current(): return False size = len(self._dict.current()) yomi = self._dict.reading() self._delete_surrounding_text(size) self._commit_string(yomi) self._reset(False) self._update() return True def _commit(self): if self._dict.current(): self._dict.confirm(self._shrunk) self._dict.reset() self._lookup_table.clear() visible = 0 < self._lookup_table.get_number_of_candidates() self.update_lookup_table(self._lookup_table, visible) self._previous_text = '' def _commit_string(self, text): if text == '゛': prev, pos = self._get_surrounding_text() if 0 < pos: found = NON_DAKU.find(prev[pos - 1]) if 0 <= found: self._delete_surrounding_text(1) text = DAKU[found] elif text == '゜': prev, pos = self._get_surrounding_text() if 0 < pos: found = NON_HANDAKU.find(prev[pos - 1]) if 0 <= found: self._delete_surrounding_text(1) text = HANDAKU[found] self._committed = text self._acked = False self._previous_text += text self.commit_text(IBus.Text.new_from_string(text)) def _reset(self, full=True): self._dict.reset() self._lookup_table.clear() self._update_lookup_table() if full: self._committed = '' self._acked = True self._previous_text = '' self._preedit_string = '' self._ignore_surrounding_text = False def _update_candidate(self): index = self._lookup_table.get_cursor_pos() size = len(self._shrunk) + len(self._dict.current()) self._dict.set_current(index) self._delete_surrounding_text(size) self._commit_string(self._shrunk + self._dict.current()) def do_page_up(self): if self._lookup_table.page_up(): self._update_lookup_table() self._update_candidate() return True def do_page_down(self): if self._lookup_table.page_down(): self._update_lookup_table() self._update_candidate() return True def do_cursor_up(self): if self._lookup_table.cursor_up(): self._update_lookup_table() self._update_candidate() return True def do_cursor_down(self): if self._lookup_table.cursor_down(): self._update_lookup_table() self._update_candidate() return True def _update(self): preedit_len = len(self._preedit_string) text = IBus.Text.new_from_string(self._preedit_string) if 0 < preedit_len: attrs = IBus.AttrList() attrs.append( IBus.Attribute.new(IBus.AttrType.UNDERLINE, IBus.AttrUnderline.SINGLE, 0, preedit_len)) text.set_attributes(attrs) # Note self.hide_preedit_text() does not seem to work as expected with Kate. # cf. "Qt5 IBus input context does not implement hide_preedit_text()", # https://bugreports.qt.io/browse/QTBUG-48412 self.update_preedit_text(text, preedit_len, 0 < preedit_len) self._update_lookup_table() def _update_lookup_table(self): if self.is_enabled(): visible = 0 < self._lookup_table.get_number_of_candidates() self.update_lookup_table(self._lookup_table, visible) def do_focus_in(self): logger.info("focus_in") self._event.reset() self.register_properties(self._prop_list) # Request the initial surrounding-text in addition to the "enable" handler. self.get_surrounding_text() def do_focus_out(self): logger.info("focus_out") self._reset() self._dict.save_orders() def do_enable(self): logger.info("enable") # Request the initial surrounding-text when enabled as documented. self.get_surrounding_text() def do_disable(self): logger.info("disable") self._reset() self._mode = 'A' self._dict.save_orders() def do_reset(self): logger.info("reset") self._reset() # Do not switch back to the Alphabet mode here; 'reset' should be # called when the text cursor is moved by a mouse click, etc. def _start_setup(self): if self._setup_pid != 0: pid, status = os.waitpid(self._setup_pid, os.WNOHANG) if pid != self._setup_pid: return self._setup_pid = 0 filename = os.path.join(package.get_libexecdir(), 'ibus-setup-hiragana') self._setup_pid = os.spawnl(os.P_NOWAIT, filename, 'ibus-setup-hiragana') def do_property_activate(self, prop_name, state): logger.info("property_activate(%s, %d)" % (prop_name, state)) if prop_name == "Setup": self._start_setup() if prop_name == "About": if self._about_dialog: self._about_dialog.present() return dialog = Gtk.AboutDialog() dialog.set_program_name(_("Hiragana IME")) dialog.set_copyright("Copyright 2017-2020 Esrille Inc.") dialog.set_authors(["Esrille Inc."]) dialog.set_documenters(["Esrille Inc."]) dialog.set_website( "file://" + os.path.join(package.get_datadir(), "help/index.html")) dialog.set_website_label(_("Introduction to Hiragana IME")) dialog.set_logo_icon_name(package.get_name()) dialog.set_version(package.get_version()) # To close the dialog when "close" is clicked, e.g. on RPi, # we connect the "response" signal to about_response_callback dialog.connect("response", self.about_response_callback) self._about_dialog = dialog dialog.show() def about_response_callback(self, dialog, response): dialog.destroy() self._about_dialog = None def set_surrounding_text_cb(self, engine, text, cursor_pos, anchor_pos): text = self.get_plain_text(text.get_text()[:cursor_pos]) if self._committed: pos = text.rfind(self._committed) if 0 <= pos and pos + len(self._committed) == len(text): self._acked = True self._committed = '' logger.debug("set_surrounding_text_cb(%s, %d, %d) => %d" % (text, cursor_pos, anchor_pos, self._acked)) def get_plain_text(self, text): plain = '' in_ruby = False for c in text: if c == IAA: in_ruby = False elif c == IAS: in_ruby = True elif c == IAT: in_ruby = False elif not in_ruby: plain += c return plain def set_cursor_location_cb(self, engine, x, y, w, h): # On Raspbian, at least till Buster, the candidate window does not # always follow the cursor position. The following code is not # necessary on Ubuntu 18.04 or Fedora 30. logger.debug("set_cursor_location_cb(%d, %d, %d, %d)" % (x, y, w, h)) self._update_lookup_table()
class EngineHiragana(EngineModeless): __gtype_name__ = 'EngineHiragana' def __init__(self): super().__init__() self._mode = 'A' # _mode must be one of _input_mode_names self._override = False self._layout = dict() self._to_kana = self._handle_default_layout self._shrunk = [] self._lookup_table = IBus.LookupTable.new(10, 0, True, False) self._lookup_table.set_orientation(IBus.Orientation.VERTICAL) self._init_props() self._settings = Gio.Settings.new('org.freedesktop.ibus.engine.hiragana') self._settings.connect('changed', self._config_value_changed_cb) self._logging_level = self._load_logging_level(self._settings) self._dict = self._load_dictionary(self._settings) self._layout = self._load_layout(self._settings) self._delay = self._load_delay(self._settings) self._event = Event(self, self._delay, self._layout) self.set_mode(self._load_input_mode(self._settings)) self._set_x4063_mode(self._load_x4063_mode(self._settings)) self._keymap = Gdk.Keymap.get_for_display(Gdk.Display.get_default()) self._keymap.connect('state-changed', self._keymap_state_changed_cb) self.connect('set-cursor-location', self._set_cursor_location_cb) self._about_dialog = None self._setup_proc = None self._q = queue.Queue() def _confirm_candidate(self): current = self._dict.current() if current: self._dict.confirm(''.join(self._shrunk)) self._dict.reset() self._lookup_table.clear() return current def _handle_default_layout(self, preedit, keyval, state=0, modifiers=0): return self._event.chr(), '' def _handle_kana_layout(self, preedit, keyval, state=0, modifiers=0): yomi = '' c = self._event.chr().lower() if self._event.is_shift(): if 'Shift' in self._layout: yomi = self._layout['Shift'].get(c, '') elif modifiers & event.SHIFT_L_BIT: yomi = self._layout['ShiftL'].get(c, '') elif modifiers & event.SHIFT_R_BIT: yomi = self._layout['ShiftR'].get(c, '') else: yomi = self._layout['Normal'].get(c, '') return yomi, preedit def _handle_roomazi_layout(self, preedit, keyval, state=0, modifiers=0): yomi = '' c = self._event.chr().lower() if preedit == 'n' and self.character_after_n.find(c) < 0: yomi = 'ん' preedit = preedit[1:] preedit += c if preedit in self._layout['Roomazi']: yomi += self._layout['Roomazi'][preedit] preedit = '' elif 2 <= len(preedit) and preedit[0] == preedit[1] and RE_SOKUON.search(preedit[1]): yomi += 'っ' preedit = preedit[1:] return yomi, preedit def _init_input_mode_props(self): props = IBus.PropList() props.append(IBus.Property(key='InputMode.Alphanumeric', prop_type=IBus.PropType.RADIO, label=IBus.Text.new_from_string(_("Alphanumeric (A)")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.CHECKED, sub_props=None)) props.append(IBus.Property(key='InputMode.Hiragana', prop_type=IBus.PropType.RADIO, label=IBus.Text.new_from_string(_("Hiragana (あ)")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None)) props.append(IBus.Property(key='InputMode.Katakana', prop_type=IBus.PropType.RADIO, label=IBus.Text.new_from_string(_("Katakana (ア)")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None)) props.append(IBus.Property(key='InputMode.WideAlphanumeric', prop_type=IBus.PropType.RADIO, label=IBus.Text.new_from_string(_("Wide Alphanumeric (A)")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None)) props.append(IBus.Property(key='InputMode.HalfWidthKatakana', prop_type=IBus.PropType.RADIO, label=IBus.Text.new_from_string(_("Halfwidth Katakana (ア)")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None)) return props def _init_props(self): self._prop_list = IBus.PropList() self._input_mode_prop = IBus.Property( key='InputMode', prop_type=IBus.PropType.MENU, symbol=IBus.Text.new_from_string(self._mode), label=IBus.Text.new_from_string(_("Input mode (%s)") % self._mode), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None) self._input_mode_prop.set_sub_props(self._init_input_mode_props()) self._prop_list.append(self._input_mode_prop) prop = IBus.Property( key='Setup', prop_type=IBus.PropType.NORMAL, label=IBus.Text.new_from_string(_("Setup")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None) self._prop_list.append(prop) prop = IBus.Property( key='Help', prop_type=IBus.PropType.NORMAL, label=IBus.Text.new_from_string(_("Help")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None) self._prop_list.append(prop) prop = IBus.Property( key='About', prop_type=IBus.PropType.NORMAL, label=IBus.Text.new_from_string(_("About Hiragana IME...")), icon=None, tooltip=None, sensitive=True, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None) self._prop_list.append(prop) def _load_delay(self, settings): delay = settings.get_int('delay') logger.info(f'delay: {delay}') return delay def _load_dictionary(self, settings, clear_history=False): path = settings.get_string('dictionary') user = settings.get_string('user-dictionary') return Dictionary(path, user, clear_history) def _load_input_mode(self, settings): mode = settings.get_string('mode') if mode not in INPUT_MODE_NAMES: mode = 'A' settings.reset('mode') logger.info(f'input mode: {mode}') return mode def _load_layout(self, settings): default_layout = os.path.join(package.get_datadir(), 'layouts') default_layout = os.path.join(default_layout, 'roomazi.json') path = settings.get_string('layout') logger.info(f'layout: {path}') layout = dict() try: with open(path) as f: layout = json.load(f) except Exception as error: logger.error(error) if not layout: try: with open(default_layout) as f: layout = json.load(f) except Exception as error: logger.error(error) if layout.get('Type') == 'Kana': self._to_kana = self._handle_kana_layout self._dict.use_romazi(False) elif 'Roomazi' in layout: self._to_kana = self._handle_roomazi_layout self._dict.use_romazi(True) else: self._to_kana = self._handle_default_layout self._dict.use_romazi(True) return layout def _load_logging_level(self, settings): level = settings.get_string('logging-level') if level not in NAME_TO_LOGGING_LEVEL: level = 'WARNING' settings.reset('logging-level') logger.info(f'logging_level: {level}') logging.getLogger().setLevel(NAME_TO_LOGGING_LEVEL[level]) return level def _load_x4063_mode(self, settings): mode = settings.get_boolean('nn-as-jis-x-4063') logger.info(f'nn_as_jis_x_4063 mode: {mode}') return mode def _lookup_dictionary(self, yomi, pos, process_n=True): if process_n and self.roman_text == 'n': yomi = yomi[:pos] + 'ん' pos += 1 self._lookup_table.clear() cand = self._dict.lookup(yomi, pos) size = len(self._dict.reading()) if 0 < size: if process_n and self.roman_text == 'n': # For FuriganaPad, yomi has to be committed anyway. self.clear_roman() self.commit_string('ん') if 1 < len(self._dict.cand()): for c in self._dict.cand(): self._lookup_table.append_candidate(IBus.Text.new_from_string(c)) return cand, size def _process_dakuten(self, c): if c not in '゛゜': return c text, pos = self.get_surrounding_string() if pos <= 0: return c if c == '゛': found = NON_DAKU.find(text[pos - 1]) if 0 <= found: self.delete_surrounding_string(1) c = DAKU[found] elif c == '゜': found = NON_HANDAKU.find(text[pos - 1]) if 0 <= found: self.delete_surrounding_string(1) c = HANDAKU[found] return c def _process_escape(self): self.clear_roman() assert self._dict.current() yomi = self._dict.reading() self._reset(False) self.commit_string(yomi) def _process_expand(self): assert self._dict.current() if not self._shrunk: return True kana = self._shrunk[-1] yomi = self._dict.reading() text, pos = self.get_surrounding_string() (cand, size) = self._lookup_dictionary(kana + yomi + text[pos:], len(kana + yomi)) assert 0 < size self.delete_surrounding_string(len(kana)) self._shrunk.pop(-1) return True def _process_katakana(self): text, pos = self.get_surrounding_string() if self.roman_text == 'n': self.clear_roman() text = text[:pos] + 'ん' pos += 1 self.commit_string('ん') for i in reversed(range(pos)): if 0 <= KATAKANA.find(text[i]): continue found = HIRAGANA.find(text[i]) if 0 <= found: self.delete_surrounding_string(pos - i) self.commit_string(KATAKANA[found] + text[i + 1:pos]) break return True def _process_okurigana(self, pos_yougen): text, pos = self.get_surrounding_string() assert pos_yougen < pos text = text[pos_yougen:pos] pos = len(text) logger.debug(f"_process_okurigana: '{text}', '{self.roman_text}'") cand, size = self._lookup_dictionary(text, pos, False) if not self._dict.current(): self._dict.create_pseudo_candidate(text) cand = text size = len(text) if self._dict.current(): self._shrunk = [] self.delete_surrounding_string(size) return True def _process_replace(self): if self._dict.current(): return True text, pos = self.get_surrounding_string() # Check Return for yôgen conversion if self._event.is_henkan() or self._event.is_key(keysyms.Return): cand, size = self._lookup_dictionary(text, pos) elif 1 <= pos: assert self._event.is_muhenkan() suffix = text[:pos].rfind('―') if 0 < suffix: cand, size = self._lookup_dictionary(text, pos) else: self.commit_string('―') text, pos = self.get_surrounding_string() cand, size = self._lookup_dictionary(text, pos) if not cand: self.delete_surrounding_string(1) if self._dict.current(): self._shrunk = [] self.delete_surrounding_string(size) return True def _process_shrink(self): logger.debug(f'_process_shrink: "{self._dict.current()}"') assert self._dict.current() yomi = self._dict.reading() if len(yomi) <= 1 or yomi[1] == '―': return True text, pos = self.get_surrounding_string() (cand, size) = self._lookup_dictionary(yomi[1:] + text[pos:], len(yomi) - 1) if 0 < size: kana = yomi[:-size] self._shrunk.append(kana) self.commit_string(kana) else: (cand, size) = self._lookup_dictionary(yomi + text[pos:], len(yomi)) return True def _process_surrounding_text(self, keyval, keycode, state, modifiers): if self._dict.current(): if keyval == keysyms.Tab: if not self._event.is_shift(): return self._process_shrink() else: return self._process_expand() if keyval == keysyms.Escape: self._process_escape() return True if keyval == keysyms.Return: current = self._confirm_candidate() self.commit_string(current) if current[-1] == '―': return self._process_replace() else: self.commit_roman() self.flush() return True if keyval == keysyms.Return and self.commit_roman(): return True if keyval == keysyms.Escape and self.clear_roman(): return True if (self._event.is_henkan() or self._event.is_muhenkan()) and not(modifiers & event.ALT_R_BIT): return self._process_replace() text, pos = self.get_surrounding_string() pos_yougen = -1 to_revert = False current = self._dict.current() if current: # Commit the current candidate yomi = self._dict.reading() self._confirm_candidate() logger.debug(f"_process_text: '{text}', current: '{current}', yomi: '{yomi}'") if current[-1] == '―': pos_yougen = pos self.commit_string(current) elif self._dict.not_selected() and (current[-1] in OKURIGANA or yomi[-1] == '―' or self.roman_text): pos_yougen = pos to_revert = True self.commit_string(current) current = yomi else: self.flush(current) self._update_preedit() if self._event.is_katakana(): self._process_katakana() return True if self._event.is_backspace(): if to_revert: if self.roman_text: self.roman_text = self.roman_text[:-1] else: current = current[:-1] text, pos = self.get_surrounding_string() self.delete_surrounding_string(pos - pos_yougen) self.commit_string(current) return self._process_okurigana(pos_yougen) if self.backspace(): return True return False yomi = '' if self._event.is_ascii(): if modifiers & event.ALT_R_BIT: yomi = self.process_alt_graph(keyval, keycode, state, modifiers) if yomi: if self.get_mode() != 'ア': yomi = to_zenkaku(yomi) self.clear_roman() elif self.get_mode() == 'A': yomi = to_zenkaku(self._event.chr()) else: yomi, self.roman_text = self._to_kana(self.roman_text, keyval, state, modifiers) if yomi: if self.get_mode() == 'ア': yomi = to_katakana(yomi) elif self.get_mode() == 'ア': yomi = to_hankaku(to_katakana(yomi)) elif keyval == keysyms.hyphen: yomi = '―' elif self.has_preedit() and self.should_draw_preedit(): if keyval == keysyms.Escape: self.clear_preedit() else: self.clear_roman() self.flush() return True else: # Let the IBus client process the key self.clear_roman() return False if to_revert and (yomi and yomi[-1] in OKURIGANA or self.roman_text): text, pos = self.get_surrounding_string() self.delete_surrounding_string(pos - pos_yougen) self.commit_string(current) yomi = self._process_dakuten(yomi) self.commit_string(yomi) if 0 <= pos_yougen and (yomi and yomi[-1] in OKURIGANA or self.roman_text): self._process_okurigana(pos_yougen) current = self._dict.current() if current and self._dict.is_complete(): self._confirm_candidate() if 1 < len(current): self.flush(current[:-1]) self.commit_string(current[-1]) else: self.flush(current) return True return True def _reset(self, full=True): self._dict.reset() self._lookup_table.clear() self._update_lookup_table() if full: self.clear() self._update_preedit() assert not self._dict.current() self._setup_sync() def _set_x4063_mode(self, on): if on: self.character_after_n = "aiueo'wyn" else: self.character_after_n = "aiueo'wy" logger.debug(f'set_x4063_mode({on})') def _update_candidate(self): index = self._lookup_table.get_cursor_pos() self._dict.set_current(index) self._update_preedit(self._dict.current()) def _update_input_mode(self): self._input_mode_prop.set_symbol(IBus.Text.new_from_string(self._mode)) self._input_mode_prop.set_label(IBus.Text.new_from_string(_("Input mode (%s)") % self._mode)) self.update_property(self._input_mode_prop) def _update_lookup_table(self): if self.is_enabled(): visible = 0 < self._lookup_table.get_number_of_candidates() self.update_lookup_table(self._lookup_table, visible) def _update_preedit(self, cand=''): preedit_text = self._preedit_text if self.should_draw_preedit() else '' text = IBus.Text.new_from_string(preedit_text + cand + self.roman_text) preedit_len = len(preedit_text) cand_len = len(cand) roman_len = len(self.roman_text) text_len = preedit_len + cand_len + roman_len attrs = IBus.AttrList() if 0 < text_len else None pos = 0 if 0 < preedit_len: attrs.append(IBus.Attribute.new(IBus.AttrType.UNDERLINE, IBus.AttrUnderline.SINGLE, pos, pos + preedit_len)) pos += preedit_len if 0 < cand_len: attrs.append(IBus.Attribute.new(IBus.AttrType.FOREGROUND, CANDIDATE_FOREGROUND_COLOR, pos, pos + cand_len)) attrs.append(IBus.Attribute.new(IBus.AttrType.BACKGROUND, CANDIDATE_BACKGROUND_COLOR, pos, pos + cand_len)) pos += cand_len if 0 < roman_len: attrs.append(IBus.Attribute.new(IBus.AttrType.UNDERLINE, IBus.AttrUnderline.SINGLE, pos, pos + roman_len)) pos += preedit_len if attrs: text.set_attributes(attrs) # A delay is necessary for textareas of Firefox 102.0.1. if text: time.sleep(EVENT_DELAY) # Note self.hide_preedit_text() does not seem to work as expected with Kate. # cf. "Qt5 IBus input context does not implement hide_preedit_text()", # https://bugreports.qt.io/browse/QTBUG-48412 self.update_preedit_text(text, text_len, 0 < text_len) self._update_lookup_table() # # setup process methods # def _setup_readline(self, process: subprocess.Popen): for line in iter(process.stdout.readline, ''): self._q.put(line.strip()) if process.poll() is not None: return def _setup_start(self): if self._setup_proc: if self._setup_proc.poll() is None: return self._setup_proc = None try: filename = os.path.join(package.get_libexecdir(), 'ibus-setup-hiragana') self._setup_proc = subprocess.Popen([filename], text=True, stdout=subprocess.PIPE) t = threading.Thread(target=self._setup_readline, args=(self._setup_proc,), daemon=True) t.start() except OSError as e: logger.error(e) except ValueError as e: logger.error(e) def _setup_sync(self): last = '' while True: try: line = self._q.get_nowait() if line == last: continue last = line logger.info(line) if line == 'reload_dictionaries': self._dict = self._load_dictionary(self._settings) elif line == 'clear_input_history': self._dict = self._load_dictionary(self._settings, clear_history=True) except queue.Empty: break # # callback methods # def _about_response_cb(self, dialog, response): dialog.destroy() self._about_dialog = None def _config_value_changed_cb(self, settings, key): logger.debug(f'config_value_changed("{key}")') if key == 'logging-level': self._logging_level = self._load_logging_level(settings) elif key == 'delay': self._reset() self._delay = self._load_delay(settings) self._event = Event(self, self._delay, self._layout) elif key == 'layout': self._reset() self._layout = self._load_layout(settings) self._event = Event(self, self._delay, self._layout) elif key == 'dictionary' or key == 'user-dictionary': self._reset() self._dict = self._load_dictionary(settings) elif key == 'mode': self.set_mode(self._load_input_mode(settings), True) elif key == 'nn-as-jis-x-4063': self._set_x4063_mode(self._load_x4063_mode(settings)) def _keymap_state_changed_cb(self, keymap): if self._event.is_onoff_by_caps(): logger.debug(f'caps lock: {keymap.get_caps_lock_state()}') if keymap.get_caps_lock_state(): self.enable_ime() else: self.disable_ime() return True def _set_cursor_location_cb(self, engine, x, y, w, h): # On Raspbian, at least till Buster, the candidate window does not # always follow the cursor position. The following code is not # necessary on Ubuntu 18.04 or Fedora 30. logger.debug(f'_set_cursor_location_cb({x}, {y}, {w}, {h})') self._update_lookup_table() # # public methods # def disable_ime(self, override=False): if self.is_enabled(): self.set_mode('A', override) return True return False def enable_ime(self, override=False): if not self.is_enabled(): self.set_mode('あ', override) return True return False def get_mode(self): return self._mode def is_enabled(self): return self.get_mode() != 'A' def is_lookup_table_visible(self): return 0 < self._lookup_table.get_number_of_candidates() def is_overridden(self): return self._override def process_alt_graph(self, keyval, keycode, state, modifiers): logger.debug(f'process_alt_graph("{self._event.chr()}")') c = self._event.chr().lower() if c == '_' and self._event._keycode == 0x0b: c = '0' if not c: return c if not self._event.is_shift(): return self._layout['\\Normal'].get(c, '') if '\\Shift' in self._layout: return self._layout['\\Shift'].get(c, '') if modifiers & event.SHIFT_L_BIT: return self._layout['\\ShiftL'].get(c, '') if modifiers & event.SHIFT_R_BIT: return self._layout['\\ShiftR'].get(c, '') def process_key_event(self, keyval, keycode, state, modifiers): logger.debug(f'process_key_event("{IBus.keyval_name(keyval)}", {keyval:#04x}, {keycode:#04x}, {state:#010x}, {modifiers:#07x})') if self._event.is_dual_role(): pass elif self._event.is_modifier(): # Ignore modifier keys return False elif state & (IBus.ModifierType.CONTROL_MASK | IBus.ModifierType.MOD1_MASK): self.clear_roman() self.flush(self._confirm_candidate()) self._update_preedit() return False self.check_surrounding_support() # Handle candidate window if 0 < self._lookup_table.get_number_of_candidates(): if keyval in (keysyms.Page_Up, keysyms.KP_Page_Up): return self.do_page_up() elif keyval in (keysyms.Page_Down, keysyms.KP_Page_Down): return self.do_page_down() elif keyval == keysyms.Up or self._event.is_muhenkan(): return self.do_cursor_up() elif keyval == keysyms.Down or self._event.is_henkan(): return self.do_cursor_down() # Cache the current surrounding text into the EngineModless's local buffer. self.get_surrounding_string() # Edit the local surrounding text buffer as we need. result = self._process_surrounding_text(keyval, keycode, state, modifiers) # Flush the local surrounding text buffer into the IBus client. if self._surrounding in (SURROUNDING_COMMITTED, SURROUNDING_SUPPORTED): self.flush() # Lastly, update the preedit text. To support LibreOffice, the # surrounding text needs to be updated before updating the preedit text. current = self._dict.current() self._update_preedit(current) return result def set_mode(self, mode, override=False): self._override = override if self._mode == mode: return False logger.debug(f'set_mode({mode})') self.clear_roman() self.flush(self._confirm_candidate()) self._update_preedit() self._mode = mode self._update_lookup_table() self._update_input_mode() return True # # virtual methods of IBus.Engine # def do_cursor_down(self): if self._lookup_table.cursor_down(): self._update_candidate() return True def do_cursor_up(self): if self._lookup_table.cursor_up(): self._update_candidate() return True def do_disable(self): logger.info('disable') self._reset() self._mode = 'A' self._dict.save_orders() def do_focus_in(self): logger.info(f'focus_in: {self._surrounding}') self._event.reset() self.register_properties(self._prop_list) self._update_preedit() super().do_focus_in() def do_focus_out(self): logger.info(f'focus_out: {self._surrounding}') if self._surrounding != SURROUNDING_BROKEN: self._reset() self._dict.save_orders() def do_page_down(self): if self._lookup_table.page_down(): self._update_candidate() return True def do_page_up(self): if self._lookup_table.page_up(): self._update_candidate() return True def do_process_key_event(self, keyval, keycode, state): return self._event.process_key_event(keyval, keycode, state) def do_property_activate(self, prop_name, state): logger.info(f'property_activate({prop_name}, {state})') if prop_name == 'Setup': self._setup_start() elif prop_name == 'Help': url = 'file://' + os.path.join(package.get_datadir(), 'help/index.html') # Use yelp to open local HTML help files. subprocess.Popen(['yelp', url]) elif prop_name == 'About': if self._about_dialog: self._about_dialog.present() return dialog = Gtk.AboutDialog() dialog.set_program_name(_("Hiragana IME")) dialog.set_copyright("Copyright 2017-2022 Esrille Inc.") dialog.set_authors(["Esrille Inc."]) dialog.set_documenters(["Esrille Inc."]) dialog.set_website("https://www.esrille.com/") dialog.set_website_label("Esrille Inc.") dialog.set_logo_icon_name(package.get_name()) dialog.set_default_icon_name(package.get_name()) dialog.set_version(package.get_version()) # To close the dialog when "close" is clicked on Raspberry Pi OS, # we connect the "response" signal to _about_response_cb dialog.connect("response", self._about_response_cb) self._about_dialog = dialog dialog.show() elif prop_name.startswith('InputMode.'): if state == IBus.PropState.CHECKED: mode = { 'InputMode.Alphanumeric': 'A', 'InputMode.Hiragana': 'あ', 'InputMode.Katakana': 'ア', 'InputMode.WideAlphanumeric': 'A', 'InputMode.HalfWidthKatakana': 'ア', }.get(prop_name, 'A') self.set_mode(mode, True) def do_reset(self): logger.info(f'reset: {self._surrounding}') if self._surrounding != SURROUNDING_BROKEN: self._reset() else: self._update_preedit()
class EngineReplaceWithKanji(IBus.Engine): __gtype_name__ = 'EngineReplaceWithKanji' def __init__(self): super(EngineReplaceWithKanji, self).__init__() self.__mode = 'A' # __mode must be one of _input_mode_names self.__override = False self.__layout = roomazi.layout self.__to_kana = self.__handle_roomazi_layout self.__preedit_string = '' self.__previous_text = '' self.__ignore_surrounding_text = False self.__lookup_table = IBus.LookupTable.new(10, 0, True, False) self.__lookup_table.set_orientation(IBus.Orientation.VERTICAL) self.__init_props() self.__config = IBus.Bus().get_config() self.__config.connect('value-changed', self.__config_value_changed_cb) self.__logging_level = self.__load_logging_level(self.__config) self.__dict = self.__load_dictionary(self.__config) self.__layout = self.__load_layout(self.__config) self.__delay = self.__load_delay(self.__config) self.__event = Event(self, self.__delay, self.__layout) self.set_mode(self.__load_input_mode(self.__config)) self.__set_x4063_mode(self.__load_x4063_mode(self.__config)) self.__shrunk = '' self.__committed = '' self.__acked = True self.connect('set-surrounding-text', self.set_surrounding_text_cb) self.connect('set-cursor-location', self.set_cursor_location_cb) def __init_props(self): self.__prop_list = IBus.PropList() self.__input_mode_prop = IBus.Property( key='InputMode', prop_type=IBus.PropType.NORMAL, symbol=IBus.Text.new_from_string(self.__mode), label=IBus.Text.new_from_string('Input mode (%s)' % self.__mode), icon=None, tooltip=None, sensitive=False, visible=True, state=IBus.PropState.UNCHECKED, sub_props=None) self.__prop_list.append(self.__input_mode_prop) def __update_input_mode(self): self.__input_mode_prop.set_symbol(IBus.Text.new_from_string(self.__mode)) self.__input_mode_prop.set_label(IBus.Text.new_from_string('Input mode (%s)' % self.__mode)) self.update_property(self.__input_mode_prop) def __load_input_mode(self, config): var = config.get_value('engine/replace-with-kanji-python', 'mode') if var is None or var.get_type_string() != 's' or not var.get_string() in _input_mode_names: mode = 'A' if var: config.unset('engine/replace-with-kanji-python', 'mode') else: mode = var.get_string() logger.info("input mode: %s", mode) return mode def __load_logging_level(self, config): var = config.get_value('engine/replace-with-kanji-python', 'logging_level') if var is None or var.get_type_string() != 's' or not var.get_string() in _name_to_logging_level: level = 'WARNING' if var: config.unset('engine/replace-with-kanji-python', 'logging_level') else: level = var.get_string() logger.info("logging_level: %s", level) logging.getLogger().setLevel(_name_to_logging_level[level]) return level def __load_dictionary(self, config): var = config.get_value('engine/replace-with-kanji-python', 'dictionary') if var is None or var.get_type_string() != 's': path = os.path.join(os.getenv('IBUS_REPLACE_WITH_KANJI_LOCATION'), 'restrained.dic') if var: config.unset('engine/replace-with-kanji-python', 'dictionary') else: path = var.get_string() return Dictionary(path) def __load_layout(self, config): var = config.get_value('engine/replace-with-kanji-python', 'layout') if var is None or var.get_type_string() != 's': path = os.path.join(os.getenv('IBUS_REPLACE_WITH_KANJI_LOCATION'), 'layouts') path = os.path.join(path, 'roomazi.json') if var: config.unset('engine/replace-with-kanji-python', 'layout') else: path = var.get_string() logger.info("layout: %s", path) layout = roomazi.layout # Use 'roomazi' as default try: with open(path) as f: layout = json.load(f) except ValueError as error: logger.error("JSON error: %s", error) except OSError as error: logger.error("Error: %s", error) except: logger.error("Unexpected error: %s %s", sys.exc_info()[0], sys.exc_info()[1]) self.__to_kana = self.__handle_roomazi_layout if 'Type' in layout: if layout['Type'] == 'Kana': self.__to_kana = self.__handle_kana_layout return layout def __load_delay(self, config): var = config.get_value('engine/replace-with-kanji-python', 'delay') if var is None or var.get_type_string() != 'i': delay = 0 if var: config.unset('engine/replace-with-kanji-python', 'delay') else: delay = var.get_int32() logger.info("delay: %d", delay) return delay def __load_x4063_mode(self, config): var = config.get_value('engine/replace-with-kanji-python', 'nn_as_jis_x_4063') if var is None or var.get_type_string() != 'b': mode = True if var: config.unset('engine/replace-with-kanji-python', 'nn_as_jis_x_4063') else: mode = var.get_boolean() logger.info("nn_as_jis_x_4063 mode: {}".format(mode)) return mode def __config_value_changed_cb(self, config, section, name, value): section = section.replace('_', '-') if section != 'engine/replace-with-kanji-python': return logger.debug("config_value_changed({}, {}, {})".format(section, name, value)) if name == "logging_level": self.__logging_level = self.__load_logging_level(config) elif name == "delay": self.__reset() self.__delay = self.__load_delay(config) self.__event = Event(self, self.__delay, self.__layout) elif name == "layout": self.__reset() self.__layout = self.__load_layout(config) self.__event = Event(self, self.__delay, self.__layout) elif name == "dictionary": self.__reset() self.__dict = self.__load_dictionary(config) elif name == "mode": self.set_mode(self.__load_input_mode(self.__config)) self.__override = True elif name == "nn_as_jis_x_4063": self.__set_x4063_mode(self.__load_x4063_mode(self.__config)) def __handle_kana_layout(self, preedit, keyval, state=0, modifiers=0): yomi = '' c = self.__event.chr().lower() if preedit == '\\': preedit = '' if self.__event.is_shift(): if 'Shift' in self.__layout: yomi = self.__layout['\\Shift'][c] elif modifiers & bits.ShiftL_Bit: yomi = self.__layout['\\ShiftL'][c] elif modifiers & bits.ShiftR_Bit: yomi = self.__layout['\\ShiftR'][c] else: yomi = self.__layout['\\Normal'][c] else: if self.__event.is_shift(): if 'Shift' in self.__layout: yomi = self.__layout['Shift'][c] elif modifiers & bits.ShiftL_Bit: yomi = self.__layout['ShiftL'][c] elif modifiers & bits.ShiftR_Bit: yomi = self.__layout['ShiftR'][c] else: yomi = self.__layout['Normal'][c] if yomi == '\\': preedit += yomi yomi = '' return yomi, preedit def __set_x4063_mode(self, on): if on: self.character_after_n = "aiueo\'wyn" else: self.character_after_n = "aiueo\'wy" logger.debug("set_x4063_mode({})".format(on)) def __handle_roomazi_layout(self, preedit, keyval, state=0, modifiers=0): yomi = '' c = self.__event.chr().lower() if preedit == 'n' and self.character_after_n.find(c) < 0: yomi = 'ん' preedit = preedit[1:] preedit += c if preedit in self.__layout['Roomazi']: yomi += self.__layout['Roomazi'][preedit] preedit = '' if yomi == '\\': preedit = yomi yomi = '' elif 2 <= len(preedit) and preedit[0] == preedit[1] and _re_tu.search(preedit[1]): yomi += 'っ' preedit = preedit[1:] return yomi, preedit def __get_surrounding_text(self): if not (self.client_capabilities & IBus.Capabilite.SURROUNDING_TEXT): self.__ignore_surrounding_text = True if self.__ignore_surrounding_text or not self.__acked: logger.debug("surrounding text: [%s]" % (self.__previous_text)) return self.__previous_text, len(self.__previous_text) tuple = self.get_surrounding_text() text = tuple[0].get_text() pos = tuple[1] # Qt reports pos as if text is in UTF-16 while GTK reports pos in sane manner. # If you're using primarily Qt, use the following code to amend the issue # when a character in Unicode supplementary planes is included in text. # # Deal with surrogate pair manually. (Qt bug?) # for i in range(len(text)): # if pos <= i: # break # if 0x10000 <= ord(text[i]): # pos -= 1 # Qt seems to insert self.__preedit_string to the text, while GTK doesn't. # We mimic GTK's behavior here. preedit_len = len(self.__preedit_string) if 0 < preedit_len and preedit_len <= pos and text[pos - preedit_len:pos] == self.__preedit_string: text = text[:-preedit_len] pos -= preedit_len logger.debug("surrounding text: '%s', %d, [%s]", text, pos, self.__previous_text) return text, pos def __delete_surrounding_text(self, size): self.__previous_text = self.__previous_text[:-size] if not self.__ignore_surrounding_text and self.__acked: self.delete_surrounding_text(-size, size) else: # Note a short delay after each BackSpace is necessary for the target application to catch up. for i in range(size): self.forward_key_event(IBus.BackSpace, 14, 0) time.sleep(0.02) self.forward_key_event(IBus.BackSpace, 14, IBus.ModifierType.RELEASE_MASK) def is_overridden(self): return self.__override def is_enabled(self): return self.get_mode() != 'A' def enable_ime(self): if not self.is_enabled(): self.set_mode('あ') return True return False def disable_ime(self): if self.is_enabled(): self.set_mode('A') return True return False def get_mode(self): return self.__mode def set_mode(self, mode): self.__override = False if self.__mode == mode: return False logger.debug("set_mode(%s)" % (mode)) self.__preedit_string = '' self.__commit() self.__mode = mode self.__update() self.__update_input_mode() return True def __is_roomazi_mode(self): return self.__to_kana == self.__handle_roomazi_layout def do_process_key_event(self, keyval, keycode, state): return self.__event.process_key_event(keyval, keycode, state) def handle_key_event(self, keyval, keycode, state, modifiers): logger.debug("handle_key_event(%s, %04x, %04x, %04x)" % (IBus.keyval_name(keyval), keycode, state, modifiers)) if self.__event.is_katakana() or self.__event.is_space() or self.__event.is_suffix() or self.__event.is_henkan(): pass elif self.__event.is_modifier(): # Ignore modifier keys return False elif state & (IBus.ModifierType.CONTROL_MASK | IBus.ModifierType.MOD1_MASK): self.__commit() return False # Handle Candidate window if 0 < self.__lookup_table.get_number_of_candidates(): if keyval == keysyms.Page_Up or keyval == keysyms.KP_Page_Up: return self.do_page_up() elif keyval == keysyms.Page_Down or keyval == keysyms.KP_Page_Down: return self.do_page_down() elif keyval == keysyms.Up or self.__event.is_muhenkan(): return self.do_cursor_up() elif keyval == keysyms.Down or self.__event.is_henkan(): return self.do_cursor_down() elif keyval == keysyms.Escape: self.handle_escape(state) return True elif keyval == keysyms.Return: self.__commit() return True if self.__preedit_string and keyval == keysyms.Escape: self.__preedit_string = '' self.__update() return True # Handle Japanese text if self.__event.is_henkan(): return self.handle_replace(keyval, state) if self.__event.is_shrink(): return self.handle_shrink(keyval, state) self.__commit() yomi = '' if self.__event.is_katakana(): if self.__event.is_shift(): self.set_mode('あ' if self.get_mode() == 'ア' else 'ア') else: self.handle_katakana() return True if self.__event.is_backspace(): if 1 <= len(self.__preedit_string): self.__preedit_string = self.__preedit_string[:-1] self.__update() return True elif 0 < len(self.__previous_text): self.__previous_text = self.__previous_text[:-1] return False if self.__event.is_ascii(): yomi, self.__preedit_string = self.__to_kana(self.__preedit_string, keyval, state, modifiers) elif keyval == keysyms.hyphen: yomi = '―' else: self.__previous_text = '' return False if yomi: if self.get_mode() == 'ア': yomi = to_katakana(yomi) self.__commit_string(yomi) self.__update() return True def lookup_dictionary(self, yomi, pos): if self.__preedit_string == 'n': yomi = yomi[:pos] + 'ん' pos += 1 self.__lookup_table.clear() cand = self.__dict.lookup(yomi, pos) size = len(self.__dict.reading()) if 0 < size: if self.__preedit_string == 'n': if self.__acked: # For furiganapad, yomi has to be committed anyway. # However, 'ん' will be acked by set_cursor_location_cb() # only after the converted text is committed later. # So we pretend that 'ん' is acked here. self.__commit_string('ん') self.__acked = True self.__committed = '' else: size = size - 1 self.__preedit_string = '' if 1 < len(self.__dict.cand()): for c in self.__dict.cand(): self.__lookup_table.append_candidate(IBus.Text.new_from_string(c)) return (cand, size) def handle_katakana(self): text, pos = self.__get_surrounding_text() for i in reversed(range(pos)): if 0 <= _katakana.find(text[i]): continue found = _hiragana.find(text[i]) if 0 <= found: self.__delete_surrounding_text(pos - i) self.__commit_string(_katakana[found] + text[i + 1:pos]) break return True def handle_replace(self, keyval, state): if not self.__dict.current(): text, pos = self.__get_surrounding_text() (cand, size) = self.lookup_dictionary(text, pos) self.__shrunk = '' else: size = len(self.__dict.current()) if not self.__event.is_shift(): cand = self.__dict.next() else: cand = self.__dict.previous() if self.__dict.current(): self.__update() self.__delete_surrounding_text(size) self.__commit_string(cand) return True def handle_shrink(self, keyval, state): logger.debug("handle_shrink: '%s'", self.__dict.current()) if not self.__dict.current(): return False yomi = self.__dict.reading() if yomi == 1: self.handle_escape(state) return True current_size = len(self.__dict.current()) text, pos = self.__get_surrounding_text() (cand, size) = self.lookup_dictionary(yomi[1:] + text[pos:], len(yomi) - 1) kana = yomi if 0 < size: kana = kana[:-size] self.__shrunk += kana elif kana[-1] == '―': kana = kana[:-1] self.__delete_surrounding_text(current_size) self.__commit_string(kana + cand) # Update preedit *after* committing the string to append preedit. self.__update() return True def handle_escape(self, state): if not self.__dict.current(): return size = len(self.__dict.current()) yomi = self.__dict.reading() self.__delete_surrounding_text(size) self.__commit_string(yomi) self.__reset(False) self.__update() def __commit(self): if self.__dict.current(): self.__dict.confirm(self.__shrunk) self.__dict.reset() self.__lookup_table.clear() visible = 0 < self.__lookup_table.get_number_of_candidates() self.update_lookup_table(self.__lookup_table, visible) self.__previous_text = '' def __commit_string(self, text): if text == '゛': prev, pos = self.__get_surrounding_text() if 0 < pos: found = _non_daku.find(prev[pos - 1]) if 0 <= found: self.__delete_surrounding_text(1) text = _daku[found] elif text == '゜': prev, pos = self.__get_surrounding_text() if 0 < pos: found = _non_handaku.find(prev[pos - 1]) if 0 <= found: self.__delete_surrounding_text(1) text = _handaku[found] self.__committed = text self.__acked = False self.__previous_text += text self.commit_text(IBus.Text.new_from_string(text)) def __reset(self, full=True): self.__dict.reset() self.__lookup_table.clear() self.__update_lookup_table() if full: self.__committed = '' self.__acked = True self.__previous_text = '' self.__preedit_string = '' self.__ignore_surrounding_text = False def __update_candidate(self): index = self.__lookup_table.get_cursor_pos() size = len(self.__dict.current()) self.__dict.set_current(index) self.__delete_surrounding_text(size) self.__commit_string(self.__dict.current()) def do_page_up(self): if self.__lookup_table.page_up(): self.__update_lookup_table() self.__update_candidate() return True def do_page_down(self): if self.__lookup_table.page_down(): self.__update_lookup_table() self.__update_candidate() return True def do_cursor_up(self): if self.__lookup_table.cursor_up(): self.__update_lookup_table() self.__update_candidate() return True def do_cursor_down(self): if self.__lookup_table.cursor_down(): self.__update_lookup_table() self.__update_candidate() return True def __update(self): preedit_len = len(self.__preedit_string) text = IBus.Text.new_from_string(self.__preedit_string) if 0 < preedit_len: attrs = IBus.AttrList() attrs.append(IBus.Attribute.new(IBus.AttrType.UNDERLINE, IBus.AttrUnderline.SINGLE, 0, preedit_len)) text.set_attributes(attrs) # Note self.hide_preedit_text() does not seem to work as expected with Kate. # cf. "Qt5 IBus input context does not implement hide_preedit_text()", # https://bugreports.qt.io/browse/QTBUG-48412 self.update_preedit_text(text, preedit_len, 0 < preedit_len) self.__update_lookup_table() def __update_lookup_table(self): if self.is_enabled(): visible = 0 < self.__lookup_table.get_number_of_candidates() self.update_lookup_table(self.__lookup_table, visible) def do_focus_in(self): logger.info("focus_in") self.__event.reset() self.register_properties(self.__prop_list) # Request the initial surrounding-text in addition to the "enable" handler. self.get_surrounding_text() def do_focus_out(self): logger.info("focus_out") self.__reset() self.__dict.save_orders() def do_enable(self): logger.info("enable") # Request the initial surrounding-text when enabled as documented. self.get_surrounding_text() def do_disable(self): logger.info("disable") self.__reset() self.__mode = 'A' self.__dict.save_orders() def do_reset(self): logger.info("reset") self.__reset() # Do not switch back to the Alphabet mode here; 'reset' should be # called when the text cursor is moved by a mouse click, etc. def do_property_activate(self, prop_name, state): logger.info("property_activate(%s, %d)" % (prop_name, state)) def set_surrounding_text_cb(self, engine, text, cursor_pos, anchor_pos): text = self.get_plain_text(text.get_text()[:cursor_pos]) if self.__committed: pos = text.rfind(self.__committed) if 0 <= pos and pos + len(self.__committed) == len(text): self.__acked = True self.__committed = '' logger.debug("set_surrounding_text_cb(%s, %d, %d) => %d" % (text, cursor_pos, anchor_pos, self.__acked)) def get_plain_text(self, text): plain = '' in_ruby = False for c in text: if c == IAA: in_ruby = False elif c == IAS: in_ruby = True elif c == IAT: in_ruby = False elif not in_ruby: plain += c return plain def set_cursor_location_cb(self, engine, x, y, w, h): # On Raspbian, at least till Buster, the candidate window does not # always follow the cursor position. The following code is not # necessary on Ubuntu 18.04 or Fedora 30. logger.debug("set_cursor_location_cb(%d, %d, %d, %d)" % (x, y, w, h)) self.__update_lookup_table()