From cdae66a661da0eb4162ae70cb6b5f9716ad8e7e1 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sat, 6 Nov 2021 16:09:41 +1100 Subject: [PATCH] keyboard: japanese: switch to 12-key flick layout This layout is far more commonly used on mobile devices, and allows for much easier typing. The keyboard primarily functions through gestures in the four cardinal directions to select which vowel kana to select. In addition, users can cycle through each kana row by tapping the key within a 2-second window (this is the equivalent to T9 input for Japanese phone keyboards). This also resolves the long-standing issue that the old keyboard did not correctly handle dakuten (there was a standalone dakuten key which added a stray dakuten mark, and the umlat mode which added dakuten to all of the keys it could) and could not input handakuten characters at all. In order to allow adding dakuten and cycling through the various modifiers for the previous kana, we need to wrap the input-box (similar to korean) but luckily we don't need any state machine magic since we just need to modify the last character in the character buffer. However because the tap timeout for T9-like-cycling needs to be reset after any non-tap key we need to add some basic wrappers around a few other input-box methods. Signed-off-by: Aleksa Sarai --- .../ui/data/keyboardlayouts/ja_keyboard.lua | 344 ++++++++++++------ .../data/keyboardlayouts/ja_keyboard_keys.lua | 339 +++++++++++++++++ tools/ja_keyboard_generate.py | 320 ++++++++++++++++ 3 files changed, 900 insertions(+), 103 deletions(-) create mode 100644 frontend/ui/data/keyboardlayouts/ja_keyboard_keys.lua create mode 100755 tools/ja_keyboard_generate.py diff --git a/frontend/ui/data/keyboardlayouts/ja_keyboard.lua b/frontend/ui/data/keyboardlayouts/ja_keyboard.lua index 3d99487fa..041abc668 100644 --- a/frontend/ui/data/keyboardlayouts/ja_keyboard.lua +++ b/frontend/ui/data/keyboardlayouts/ja_keyboard.lua @@ -1,113 +1,251 @@ +-------- +-- Japanese 12-key flick keyboard layout, modelled after Android's flick +-- keyboard. Rather than being modal, it has the ability to apply modifiers to +-- the previous character. In addition, users can tap a kana key to cycle +-- through the various kana in that kana row (and associated small kana). +-- +-- Note that because we cannot have tri-state buttons (and we want to be able +-- to input katakana) we emulate a quad-state button using the symbol and shift +-- layers. Users just have to tap whatever mode they want and they should be +-- able to get there easily. +-------- + +local TimeVal = require("ui/timeval") +local logger = require("logger") +local util = require("util") +local _ = require("gettext") +local N_ = _.ngettext +local T = require("ffi/util").template + +local K = require("frontend/ui/data/keyboardlayouts/ja_keyboard_keys") + +local DEFAULT_KEITAI_TAP_INTERVAL = 2 + +-- "Keitai input" is an input mode similar to T9 mobile input, where you tap a +-- key to cycle through several candidate characters. The tap interval is how +-- long we are going to wait before committing to the current character. See +-- for more +-- information. + +local function getKeitaiTapInterval() + return G_reader_settings:readSetting("keyboard_japanese_keitai_tap_interval") or DEFAULT_KEITAI_TAP_INTERVAL +end + +local function setKeitaiTapInterval(interval) + G_reader_settings:saveSetting("keyboard_japanese_keitai_tap_interval", interval) +end + +local function exitKeitaiMode(inputbox) + logger.dbg("ja_kbd: clearing keitai window last tap tv") + inputbox._ja_last_tap_tv = nil +end + +local function wrappedAddChars(inputbox, char) + -- Find the relevant modifier cycle tables. + local modifier_table = K.MODIFIER_TABLE[char] + local keitai_cycle = K.KEITAI_TABLE[char] + + -- For keitai buttons, are we still in the tap interval? + local within_tap_window + if keitai_cycle then + if inputbox._ja_last_tap_tv then + within_tap_window = TimeVal:getDuration(inputbox._ja_last_tap_tv) < getKeitaiTapInterval() + end + inputbox._ja_last_tap_tv = TimeVal:now() + else + -- This is a non-keitai or non-tap key, so break out of keitai window. + exitKeitaiMode(inputbox) + end + + -- Get the character behind the cursor and figure out how to modify it. + local new_char + local current_char = inputbox:getChar(-1) + if modifier_table then + new_char = modifier_table[current_char] + elseif keitai_cycle and keitai_cycle[current_char] and within_tap_window then + new_char = keitai_cycle[current_char] + else + -- Regular key, just add it as normal. + inputbox.addChars:raw_method_call(char) + return + end + + -- Replace character if there was a valid replacement. + logger.dbg("ja_kbd: applying", char, "key to", current_char, "yielded", new_char or "") + if not current_char then return end -- no character to modify + if new_char then + -- Use the raw methods to avoid calling the callbacks. + inputbox.delChar:raw_method_call() + inputbox.addChars:raw_method_call(new_char) + end +end + +local function wrapInputBox(inputbox) + if inputbox._ja_wrapped == nil then + inputbox._ja_wrapped = true + local wrappers = {} + + -- Wrap all of the navigation and non-single-character-input keys with + -- a callback to clear the tap window, but pass through to the + -- original function. + + -- Delete text. + table.insert(wrappers, util.wrapMethod(inputbox, "delChar", nil, exitKeitaiMode)) + table.insert(wrappers, util.wrapMethod(inputbox, "delToStartOfLine", nil, exitKeitaiMode)) + table.insert(wrappers, util.wrapMethod(inputbox, "clear", nil, exitKeitaiMode)) + -- Navigation. + table.insert(wrappers, util.wrapMethod(inputbox, "leftChar", nil, exitKeitaiMode)) + table.insert(wrappers, util.wrapMethod(inputbox, "rightChar", nil, exitKeitaiMode)) + table.insert(wrappers, util.wrapMethod(inputbox, "upLine", nil, exitKeitaiMode)) + table.insert(wrappers, util.wrapMethod(inputbox, "downLine", nil, exitKeitaiMode)) + -- Move to other input box. + table.insert(wrappers, util.wrapMethod(inputbox, "unfocus", nil, exitKeitaiMode)) + -- Gestures to move cursor. + table.insert(wrappers, util.wrapMethod(inputbox, "onTapTextBox", nil, exitKeitaiMode)) + table.insert(wrappers, util.wrapMethod(inputbox, "onHoldTextBox", nil, exitKeitaiMode)) + table.insert(wrappers, util.wrapMethod(inputbox, "onSwipeTextBox", nil, exitKeitaiMode)) + + -- addChars is the only method we need a more complicated wrapper for. + table.insert(wrappers, util.wrapMethod(inputbox, "addChars", wrappedAddChars, nil)) + + return function() + if inputbox._ja_wrapped then + for _, wrapper in ipairs(wrappers) do + wrapper:revert() + end + inputbox._ja_last_tap_tv = nil + inputbox._ja_wrapped = nil + end + end + end +end + +local function genMenuItems(self) + return { + { + text_func = function() + local interval = getKeitaiTapInterval() + if interval ~= 0 then + -- @translators Keitai input is a kind of Japanese keyboard input mode (similar to T9 keypad input). See for more information. + return T(N_("Keitai tap interval: %1 second", "Keitai tap interval: %1 seconds", interval), interval) + else + -- @translators Flick and keitai are kinds of Japanese keyboard input modes. See for more information. + return _("Keitai input: disabled (flick-only input)") + end + end, + help_text = _("How long to wait for the next tap when in keitai input mode before committing to the current character. During this window, tapping a single key will loop through candidates for the current character being input. Any other input will cause you to leave keitai mode."), + keep_menu_open = true, + callback = function(touchmenu_instance) + local SpinWidget = require("ui/widget/spinwidget") + local UIManager = require("ui/uimanager") + local Screen = require("device").screen + local items = SpinWidget:new{ + title_text = _("Keitai tap interval"), + info_text = T(_([[ +How long to wait (in seconds) for the next tap when in keitai input mode before committing to the current character. During this window, tapping a single key will loop through candidates for the current character being input. Any other input will cause you to leave keitai mode. + +If set to 0, keitai input is disabled entirely and only flick input can be used. + +Default value: %1]]), DEFAULT_KEITAI_TAP_INTERVAL), + width = math.floor(Screen:getWidth() * 0.75), + value = getKeitaiTapInterval(), + value_min = 0, + value_max = 10, + value_step = 1, + ok_text = _("Set interval"), + default_value = DEFAULT_KEITAI_TAP_INTERVAL, + callback = function(spin) + setKeitaiTapInterval(spin.value) + if touchmenu_instance then touchmenu_instance:updateItems() end + end, + } + UIManager:show(items) + end, + }, + } +end + +-- Basic modifier keys. +local M_l = { label = "←", } -- Arrow left +local M_r = { label = "→", } -- Arrow right +local Msw = { label = "🌐", } -- Switch keyboard +local Mbk = { label = "", bold = false, } -- Backspace + +-- Modifier key for kana input. +local Mmd = { label = "◌゙ ◌゚", alt_label = "大⇔小", + K.MODIFIER_KEY_CYCLIC, + west = K.MODIFIER_KEY_DAKUTEN, + north = K.MODIFIER_KEY_SMALLKANA, + east = K.MODIFIER_KEY_HANDAKUTEN, } +-- Modifier key for latin input. +local Msh = { label = "a⇔A", + K.MODIFIER_KEY_SHIFT } + +-- In order to emulate the tri-modal system of 12-key keyboards we treat shift +-- and symbol modes as being used to specify which of the three target layers +-- to use. The four modes are hiragana (default), katakana (shift), English +-- letters (symbol), numbers and symbols (shift+symbol). +-- +-- In order to make it easy for users to know which button will take them to a +-- specific mode, we need to give different keys the same name at certain +-- times, so we append a \0 to one set so that the VirtualKeyboard can +-- differentiate them on key tap even though they look the same to the user. + +-- Shift-mode toggle button. +local Sh_abc = { label = "ABC\0", alt_label = "ひらがな", bold = true, } +local Sh_sym = { label = "記号\0", bold = true, } -- Switch to numbers and symbols. +local Sh_hir = { label = "ひらがな\0", bold = true, } -- Switch to hiragana. +local Sh_kat = { label = "カタカナ\0", bold = true, } -- Switch to katakana. +-- Symbol-mode toggle button. +local Sy_abc = { label = "ABC", alt_label = "記号", bold = true, } +local Sy_sym = { label = "記号", bold = true, } -- Switch to numbers and symbols. +local Sy_hir = { label = "ひらがな", bold = true, } -- Switch to hiragana. +local Sy_kat = { label = "カタカナ", bold = true, } -- Switch to katakana. + return { min_layer = 1, - max_layer = 10, - shiftmode_keys = {[""] = true}, - symbolmode_keys = {["記号"] = true, ["かな"] = true}, + max_layer = 4, + shiftmode_keys = {["ABC\0"] = true, ["記号\0"] = true, ["カタカナ\0"] = true, ["ひらがな\0"] = true}, + symbolmode_keys = {["ABC"] = true, ["記号"] = true, ["ひらがな"] = true, ["カタカナ"] = true}, utf8mode_keys = {["🌐"] = true}, - umlautmode_keys = {["゜"] = true}, keys = { - -- first row - { -- 1 2 3 4 5 6 7 8 9 10 - { "ア", "あ", "~", "`", "ア", "あ", "~", "`", "ア", "あ", }, - { "カ", "か", "!", "1", "ガ", "が", "!", "1", "カ", "か", }, - { "サ", "さ", "@", "2", "ザ", "ざ", "@", "2", "サ", "さ", }, - { "タ", "た", "#", "3", "ダ", "だ", "#", "3", "タ", "た" }, - { "ナ", "な", "$", "4", "ナ", "な", "$", "4", "ナ", "な", }, - { "ハ", "は", "%", "5", "バ", "ば", "%", "5", "パ", "ぱ", }, - { "マ", "ま", "^", "6", "マ", "ま", "^", "6", "マ", "ま", }, - { "ヤ", "や", "&", "7", "ヤ", "や", "&", "7", "ヤ", "や", }, - { "ラ", "ら", "*", "8", "ラ", "ら", "*", "8", "ラ", "ら", }, - { "ワ", "わ", "(", "9", "ワ", "わ", "(", "9", "ワ", "わ", }, - { "ァ", "ぁ", ")", "0", "ァ", "ぁ", ")", "0", "ァ", "ぁ", }, - { "ャ", "ゃ", "_", "-", "ャ", "ゃ", "_", "-", "ャ", "ゃ", }, - { "゛", "゛", "+", "=", "゛", "゛", "+", "=", "゛", "゛", }, - }, - -- second row - { -- 1 2 3 4 5 6 7 8 9 10 - { "イ", "い", "Q", "q", "イ", "い", "Q", "q", "イ", "い", }, - { "キ", "き", "W", "w", "ギ", "ぎ", "W", "w", "キ", "き", }, - { "シ", "し", "E", "e", "ジ", "じ", "E", "e", "シ", "し", }, - { "チ", "ち", "R", "r", "ヂ", "ぢ", "R", "r", "チ", "ち", }, - { "ニ", "に", "T", "t", "ニ", "に", "T", "t", "ニ", "に", }, - { "ヒ", "ひ", "Y", "y", "ビ", "び", "Y", "y", "ピ", "ぴ", }, - { "ミ", "み", "U", "u", "ミ", "み", "U", "u", "ミ", "み", }, - { " ", " ", "I", "i", " ", " ", "I", "i", " ", " ", }, - { "リ", "り", "O", "o", "リ", "り", "O", "o", "リ", "り", }, - { " ", " ", "P", "p", " ", " ", "P", "p", " ", " ", }, - { "ィ", "ぃ", "{", "[", "ィ", "ぃ", "{", "[", "ィ", "ぃ", }, - { "ュ", "ゅ", "}", "]", "ュ", "ゅ", "}", "]", "ュ", "ゅ", }, - { "゜", "゜", "|", "\\", "゜", "゜", "|", "\\", "゜", "゜", }, + -- first row [🌐, あ, か, さ, ] + { -- R r S s + Msw, + { K.k_a, K.h_a, K.s_1, K.l_1, }, + { K.kKa, K.hKa, K.s_2, K.l_2, }, + { K.kSa, K.hSa, K.s_3, K.l_3, }, + Mbk, }, - -- third row - { -- 1 2 3 4 5 6 7 8 9 10 - { "ウ", "う", "A", "a", "ヴ", "ゔ", "A", "a", "ウ", "う", }, - { "ク", "く", "S", "s", "グ", "ぐ", "S", "s", "ク", "く", }, - { "ス", "す", "D", "d", "ズ", "ず", "D", "d", "ス", "す", }, - { "ツ", "つ", "F", "f", "ヅ", "づ", "F", "f", "ツ", "つ", }, - { "ヌ", "ぬ", "G", "g", "ヌ", "ぬ", "G", "g", "ヌ", "ぬ", }, - { "フ", "ふ", "H", "h", "ブ", "ぶ", "H", "h", "プ", "ぷ", }, - { "ム", "む", "J", "j", "ム", "む", "J", "j", "ム", "む", }, - { "ユ", "ゆ", "K", "k", "ユ", "ゆ", "K", "k", "ユ", "ゆ", }, - { "ル", "る", "L", "l", "ル", "る", "L", "l", "ル", "る", }, - { "ヲ", "を", ":", ";", "ヲ", "を", ":", ";", "ヲ", "を", }, - { "ゥ", "ぅ", "\"", "'", "ゥ", "ぅ", "\"", "'", "ゥ", "ぅ", }, - { "ョ", "ょ", "『", "「", "ョ", "ょ", "『", "「", "ョ", "ょ", }, - { "ー", "ー", "』", "」", "ー", "ー", "』", "」", "ー", "ー", }, + -- second row [←, た, な, は, →] + { -- R r S s + M_l, + { K.kTa, K.hTa, K.s_4, K.l_4, }, + { K.kNa, K.hNa, K.s_5, K.l_5, }, + { K.kHa, K.hHa, K.s_6, K.l_6, }, + M_r, }, - -- fourth row - { -- 1 2 3 4 5 6 7 8 9 10 - { "エ", "え", "Z", "z", "エ", "え", "Z", "z", "エ", "え", }, - { "ケ", "け", "X", "x", "ゲ", "げ", "X", "x", "ケ", "け", }, - { "セ", "せ", "C", "c", "ゼ", "ぜ", "C", "c", "セ", "せ", }, - { "テ", "て", "V", "v", "デ", "で", "V", "v", "テ", "て", }, - { "ネ", "ね", "B", "b", "ネ", "ね", "B", "b", "ネ", "ね", }, - { "ヘ", "へ", "N", "n", "ベ", "べ", "N", "n", "ペ", "ぺ", }, - { "メ", "め", "M", "m", "メ", "め", "M", "m", "メ", "め", }, - { " ", " ", "<", ",", " ", " ", "<", ",", " ", " ", }, - { "レ", "れ", ">", ".", "レ", "れ", ">", ".", "レ", "れ", }, - { " ", " ", "?", "/", " ", " ", "?", "/", " ", " ", }, - { "ェ", "ぇ", "~", "・", "ェ", "ぇ", "~", "・", "ェ", "ぇ", }, - { "ッ", "っ", "…", "、", "ッ", "っ", "…", "、", "ッ", "っ", }, - { "、", "、", "¥", "。", "、", "、", "¥", "。", "、", "、", }, + -- third row [, ま, や, ら, < >] + { -- R r S s + { Sh_hir, Sh_kat, Sh_abc, Sh_sym, }, -- Shift + { K.kMa, K.hMa, K.s_7, K.l_7, }, + { K.kYa, K.hYa, K.s_8, K.l_8, }, + { K.kRa, K.hRa, K.s_9, K.l_9, }, + { label = "␣", + " ", " ", " ", " ",} -- whitespace }, - -- fifth row - { -- 1 2 3 4 5 6 7 8 9 10 - { "オ", "お", "Á", "á", "オ", "お", "Á", "á", "オ", "お", }, - { "コ", "こ", "É", "é", "ゴ", "ご", "É", "é", "コ", "こ", }, - { "ソ", "そ", "Í", "í", "ゾ", "ぞ", "Í", "í", "ソ", "そ", }, - { "ト", "と", "Ó", "ó", "ド", "ど", "Ó", "ó", "ト", "と", }, - { "ノ", "の", "Ú", "ú", "ノ", "の", "Ú", "ú", "ノ", "の", }, - { "ホ", "ほ", "Ñ", "ñ", "ボ", "ぼ", "Ñ", "ñ", "ホ", "ほ", }, - { "モ", "も", "Ü", "ü", "モ", "も", "Ü", "ü", "モ", "も", }, - { "ヨ", "よ", "¿", "ç", "ヨ", "よ", "¿", "ç", "ヨ", "よ", }, - { "ロ", "ろ", "¡", "ß", "ロ", "ろ", "¡", "ß", "ロ", "ろ", }, - { "ン", "ん", "Æ", "æ", "ン", "ん", "Æ", "æ", "ン", "ん", }, - { "ォ", "ぉ", "€", "£", "ォ", "ぉ", "€", "£", "ォ", "ぉ", }, - { " ", " ", "«", "【", " ", " ", "«", "”", " ", " ", }, - { "。", "。", "»", "】", "。", "。", "’", "”", "。", "。", }, - }, - -- sixth row - { - { label = "", - width = 1.5 - }, - { label = "🌐", - width = 1.5 - }, - { "記号", "記号", "かな", "かな", "記号", "記号", "かな", "かな", "記号", "記号", - width = 1.5}, - { label = "空白", - " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - width = 5.5}, - { label = "⮠", - "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", - width = 1.5, - bold = true - }, - { label = "", - width = 1.5, - bold = false - }, + -- fourth row [symbol, modifier, わ, 。, enter] + { -- R r S s + { Sy_sym, Sy_abc, Sy_kat, Sy_hir, }, -- Symbols + { Mmd, Mmd, K.s_b, Msh, }, + { K.kWa, K.hWa, K.s_0, K.l_0, }, + { K.k_P, K.h_P, K.s_p, K.l_P, }, + { label = "⮠", bold = true, + "\n", "\n", "\n", "\n",}, -- newline }, }, + + -- Methods. + wrapInputBox = wrapInputBox, + genMenuItems = genMenuItems, } diff --git a/frontend/ui/data/keyboardlayouts/ja_keyboard_keys.lua b/frontend/ui/data/keyboardlayouts/ja_keyboard_keys.lua new file mode 100644 index 000000000..1ec2055e6 --- /dev/null +++ b/frontend/ui/data/keyboardlayouts/ja_keyboard_keys.lua @@ -0,0 +1,339 @@ + +--- @note This file was generated with tools/ja_keyboard_generate.py. +-- DO NOT EDIT THIS FILE MANUALLY. Instead, edit and re-run the script. + +-- These values are displayed to users when they long-press on the modifier +-- key, so make them somewhat understandable (変換 is not the best word to use +-- for the cycle button because it's fairly generic and in IMEs it usually +-- indicates cycling through the IME suggestions but I couldn't find any +-- documentation about the 12-key keyboard that uses a more specific term). + +local MODIFIER_CYCLIC = "変換" +local MODIFIER_DAKUTEN = "◌゙" +local MODIFIER_HANDAKUTEN = "◌゚" +local MODIFIER_SMALLKANA = "小" +local MODIFIER_SHIFT = "" + +return { + -- Keypad definitions. + h_a = { "あ", west = "い", north = "う", east = "え", south = "お" }, + hKa = { "か", west = "き", north = "く", east = "け", south = "こ" }, + hSa = { "さ", west = "し", north = "す", east = "せ", south = "そ" }, + hTa = { "た", west = "ち", north = "つ", east = "て", south = "と" }, + hNa = { "な", west = "に", north = "ぬ", east = "ね", south = "の" }, + hHa = { "は", west = "ひ", north = "ふ", east = "へ", south = "ほ" }, + hMa = { "ま", west = "み", north = "む", east = "め", south = "も" }, + hYa = { alt_label = "()", + "や", west = "(", north = "ゆ", east = ")", south = "よ" }, + hRa = { "ら", west = "り", north = "る", east = "れ", south = "ろ" }, + hWa = { alt_label = "ー〜", + "わ", west = "を", north = "ん", east = "ー", south = "〜" }, + h_P = { alt_label = "。?!…", + "、", west = "。", north = "?", east = "!", south = "…" }, + k_a = { "ア", west = "イ", north = "ウ", east = "エ", south = "オ" }, + kKa = { "カ", west = "キ", north = "ク", east = "ケ", south = "コ" }, + kSa = { "サ", west = "シ", north = "ス", east = "セ", south = "ソ" }, + kTa = { "タ", west = "チ", north = "ツ", east = "テ", south = "ト" }, + kNa = { "ナ", west = "ニ", north = "ヌ", east = "ネ", south = "ノ" }, + kHa = { "ハ", west = "ヒ", north = "フ", east = "ヘ", south = "ホ" }, + kMa = { "マ", west = "ミ", north = "ム", east = "メ", south = "モ" }, + kYa = { alt_label = "()", + "ヤ", west = "(", north = "ユ", east = ")", south = "ヨ" }, + kRa = { "ラ", west = "リ", north = "ル", east = "レ", south = "ロ" }, + kWa = { alt_label = "ー〜", + "ワ", west = "ヲ", north = "ン", east = "ー", south = "〜" }, + k_P = { alt_label = "。?!…", + "、", west = "。", north = "?", east = "!", south = "…" }, + l_1 = { label = "@-_/", alt_label = "1", + "@", west = "-", north = "_", east = "/", south = "1" }, + l_2 = { label = "abc", alt_label = "2", + "a", west = "b", north = "c", east = "2" }, + l_3 = { label = "def", alt_label = "3", + "d", west = "e", north = "f", east = "3" }, + l_4 = { label = "ghi", alt_label = "4", + "g", west = "h", north = "i", east = "4" }, + l_5 = { label = "jkl", alt_label = "5", + "j", west = "k", north = "l", east = "5" }, + l_6 = { label = "mno", alt_label = "6", + "m", west = "n", north = "o", east = "6" }, + l_7 = { label = "pqrs", alt_label = "7", + "p", west = "q", north = "r", east = "s", south = "7" }, + l_8 = { label = "tuv", alt_label = "8", + "t", west = "u", north = "v", east = "8" }, + l_9 = { label = "wxyz", alt_label = "9", + "w", west = "x", north = "y", east = "z", south = "9" }, + l_0 = { label = "'\":;", alt_label = "0", + "'", west = "\"", north = ":", east = ";", south = "0" }, + l_P = { label = ",.?!", + ",", west = ".", north = "?", east = "!" }, + s_1 = { alt_label = "☆♪", + "1", west = "☆", north = "♪" }, + s_2 = { alt_label = "¥$€", + "2", west = "¥", north = "$", east = "€" }, + s_3 = { alt_label = "%゜#", + "3", west = "%", north = "゜", east = "#" }, + s_4 = { alt_label = "○*・", + "4", west = "○", north = "*", east = "・" }, + s_5 = { alt_label = "+×÷", + "5", west = "+", north = "×", east = "÷" }, + s_6 = { alt_label = "<=>", + "6", west = "<", north = "=", east = ">" }, + s_7 = { alt_label = "「」:", + "7", west = "「", north = "」", east = ":" }, + s_8 = { alt_label = "〒々〆", + "8", west = "〒", north = "々", east = "〆" }, + s_9 = { alt_label = "^|\\", + "9", west = "^", north = "|", east = "\\" }, + s_0 = { alt_label = "~…@", + "0", west = "~", north = "…", east = "@" }, + s_b = { label = "()[]", + "(", west = ")", north = "[", east = "]" }, + s_p = { label = ".,-/", + ".", west = ",", north = "-", east = "/" }, + + -- Cycle lookup table for keitai (multi-tap) keypad input. + KEITAI_TABLE = { + ["あ"] = { ["あ"] = "い", ["い"] = "う", ["う"] = "え", ["え"] = "お", ["お"] = "ぁ", ["ぁ"] = "ぃ", ["ぃ"] = "ぅ", ["ぅ"] = "ぇ", ["ぇ"] = "ぉ", ["ぉ"] = "あ", }, + ["か"] = { ["か"] = "き", ["き"] = "く", ["く"] = "け", ["け"] = "こ", ["こ"] = "か", }, + ["さ"] = { ["さ"] = "し", ["し"] = "す", ["す"] = "せ", ["せ"] = "そ", ["そ"] = "さ", }, + ["た"] = { ["た"] = "ち", ["ち"] = "つ", ["つ"] = "て", ["て"] = "と", ["と"] = "っ", ["っ"] = "た", }, + ["な"] = { ["な"] = "に", ["に"] = "ぬ", ["ぬ"] = "ね", ["ね"] = "の", ["の"] = "な", }, + ["は"] = { ["は"] = "ひ", ["ひ"] = "ふ", ["ふ"] = "へ", ["へ"] = "ほ", ["ほ"] = "は", }, + ["ま"] = { ["ま"] = "み", ["み"] = "む", ["む"] = "め", ["め"] = "も", ["も"] = "ま", }, + ["や"] = { ["や"] = "ゆ", ["ゆ"] = "よ", ["よ"] = "ゃ", ["ゃ"] = "ゅ", ["ゅ"] = "ょ", ["ょ"] = "や", }, + ["ら"] = { ["ら"] = "り", ["り"] = "る", ["る"] = "れ", ["れ"] = "ろ", ["ろ"] = "ら", }, + ["わ"] = { ["わ"] = "を", ["を"] = "ん", ["ん"] = "ゎ", ["ゎ"] = "ー", ["ー"] = "〜", ["〜"] = "わ", }, + ["、"] = { ["、"] = "。", ["。"] = "?", ["?"] = "!", ["!"] = "…", ["…"] = "・", ["・"] = " ", [" "] = "、", }, + ["ア"] = { ["ア"] = "イ", ["イ"] = "ウ", ["ウ"] = "エ", ["エ"] = "オ", ["オ"] = "ァ", ["ァ"] = "ィ", ["ィ"] = "ゥ", ["ゥ"] = "ェ", ["ェ"] = "ォ", ["ォ"] = "ア", }, + ["カ"] = { ["カ"] = "キ", ["キ"] = "ク", ["ク"] = "ケ", ["ケ"] = "コ", ["コ"] = "カ", }, + ["サ"] = { ["サ"] = "シ", ["シ"] = "ス", ["ス"] = "セ", ["セ"] = "ソ", ["ソ"] = "サ", }, + ["タ"] = { ["タ"] = "チ", ["チ"] = "ツ", ["ツ"] = "テ", ["テ"] = "ト", ["ト"] = "ッ", ["ッ"] = "タ", }, + ["ナ"] = { ["ナ"] = "ニ", ["ニ"] = "ヌ", ["ヌ"] = "ネ", ["ネ"] = "ノ", ["ノ"] = "ナ", }, + ["ハ"] = { ["ハ"] = "ヒ", ["ヒ"] = "フ", ["フ"] = "ヘ", ["ヘ"] = "ホ", ["ホ"] = "ハ", }, + ["マ"] = { ["マ"] = "ミ", ["ミ"] = "ム", ["ム"] = "メ", ["メ"] = "モ", ["モ"] = "マ", }, + ["ヤ"] = { ["ヤ"] = "ユ", ["ユ"] = "ヨ", ["ヨ"] = "ャ", ["ャ"] = "ュ", ["ュ"] = "ョ", ["ョ"] = "ヤ", }, + ["ラ"] = { ["ラ"] = "リ", ["リ"] = "ル", ["ル"] = "レ", ["レ"] = "ロ", ["ロ"] = "ラ", }, + ["ワ"] = { ["ワ"] = "ヲ", ["ヲ"] = "ン", ["ン"] = "ヮ", ["ヮ"] = "ー", ["ー"] = "〜", ["〜"] = "ワ", }, + ["@"] = { ["@"] = "-", ["-"] = "_", ["_"] = "/", ["/"] = "1", ["1"] = "@", }, + ["a"] = { ["a"] = "b", ["b"] = "c", ["c"] = "A", ["A"] = "B", ["B"] = "C", ["C"] = "2", ["2"] = "a", }, + ["d"] = { ["d"] = "e", ["e"] = "f", ["f"] = "D", ["D"] = "E", ["E"] = "F", ["F"] = "3", ["3"] = "d", }, + ["g"] = { ["g"] = "h", ["h"] = "i", ["i"] = "G", ["G"] = "H", ["H"] = "I", ["I"] = "4", ["4"] = "g", }, + ["j"] = { ["j"] = "k", ["k"] = "l", ["l"] = "J", ["J"] = "K", ["K"] = "L", ["L"] = "5", ["5"] = "j", }, + ["m"] = { ["m"] = "n", ["n"] = "o", ["o"] = "M", ["M"] = "N", ["N"] = "O", ["O"] = "6", ["6"] = "m", }, + ["p"] = { ["p"] = "q", ["q"] = "r", ["r"] = "s", ["s"] = "P", ["P"] = "Q", ["Q"] = "R", ["R"] = "S", ["S"] = "7", ["7"] = "p", }, + ["t"] = { ["t"] = "u", ["u"] = "v", ["v"] = "T", ["T"] = "U", ["U"] = "V", ["V"] = "8", ["8"] = "t", }, + ["w"] = { ["w"] = "x", ["x"] = "y", ["y"] = "z", ["z"] = "W", ["W"] = "X", ["X"] = "Y", ["Y"] = "Z", ["Z"] = "9", ["9"] = "w", }, + ["'"] = { ["'"] = "\"", ["\""] = ":", [":"] = ";", [";"] = "0", ["0"] = "'", }, + [","] = { [","] = ".", ["."] = "?", ["?"] = "!", ["!"] = ",", }, + }, + + -- Special keycodes for the cyclic keys. + MODIFIER_KEY_CYCLIC = MODIFIER_CYCLIC, + MODIFIER_KEY_DAKUTEN = MODIFIER_DAKUTEN, + MODIFIER_KEY_HANDAKUTEN = MODIFIER_HANDAKUTEN, + MODIFIER_KEY_SMALLKANA = MODIFIER_SMALLKANA, + MODIFIER_KEY_SHIFT = MODIFIER_SHIFT, + + -- Modifier lookup table. + MODIFIER_TABLE = { + [MODIFIER_CYCLIC] = { + ["あ"] = "ぁ", ["ぁ"] = "あ", + ["い"] = "ぃ", ["ぃ"] = "い", + ["え"] = "ぇ", ["ぇ"] = "え", + ["う"] = "ぅ", ["ぅ"] = "ゔ", ["ゔ"] = "う", + ["お"] = "ぉ", ["ぉ"] = "お", + ["か"] = "が", ["が"] = "か", + ["き"] = "ぎ", ["ぎ"] = "き", + ["く"] = "ぐ", ["ぐ"] = "く", + ["け"] = "げ", ["げ"] = "け", + ["こ"] = "ご", ["ご"] = "こ", + ["さ"] = "ざ", ["ざ"] = "さ", + ["し"] = "じ", ["じ"] = "し", + ["す"] = "ず", ["ず"] = "す", + ["せ"] = "ぜ", ["ぜ"] = "せ", + ["そ"] = "ぞ", ["ぞ"] = "そ", + ["た"] = "だ", ["だ"] = "た", + ["ち"] = "ぢ", ["ぢ"] = "ち", + ["つ"] = "っ", ["っ"] = "づ", ["づ"] = "つ", + ["て"] = "で", ["で"] = "て", + ["と"] = "ど", ["ど"] = "と", + ["は"] = "ば", ["ば"] = "ぱ", ["ぱ"] = "は", + ["ひ"] = "び", ["び"] = "ぴ", ["ぴ"] = "ひ", + ["ふ"] = "ぶ", ["ぶ"] = "ぷ", ["ぷ"] = "ふ", + ["へ"] = "べ", ["べ"] = "ぺ", ["ぺ"] = "へ", + ["ほ"] = "ぼ", ["ぼ"] = "ぽ", ["ぽ"] = "ほ", + ["や"] = "ゃ", ["ゃ"] = "や", + ["ゆ"] = "ゅ", ["ゅ"] = "ゆ", + ["よ"] = "ょ", ["ょ"] = "よ", + ["わ"] = "ゎ", ["ゎ"] = "わ", + ["ア"] = "ァ", ["ァ"] = "ア", + ["イ"] = "ィ", ["ィ"] = "イ", + ["ウ"] = "ゥ", ["ゥ"] = "ヴ", ["ヴ"] = "ウ", + ["エ"] = "ェ", ["ェ"] = "エ", + ["オ"] = "ォ", ["ォ"] = "オ", + ["カ"] = "ガ", ["ガ"] = "カ", + ["キ"] = "ギ", ["ギ"] = "キ", + ["ク"] = "グ", ["グ"] = "ク", + ["ケ"] = "ゲ", ["ゲ"] = "ケ", + ["コ"] = "ゴ", ["ゴ"] = "コ", + ["サ"] = "ザ", ["ザ"] = "サ", + ["シ"] = "ジ", ["ジ"] = "シ", + ["ス"] = "ズ", ["ズ"] = "ス", + ["セ"] = "ゼ", ["ゼ"] = "セ", + ["ソ"] = "ゾ", ["ゾ"] = "ソ", + ["タ"] = "ダ", ["ダ"] = "タ", + ["チ"] = "ヂ", ["ヂ"] = "チ", + ["ツ"] = "ッ", ["ッ"] = "ヅ", ["ヅ"] = "ツ", + ["テ"] = "デ", ["デ"] = "テ", + ["ト"] = "ド", ["ド"] = "ト", + ["ハ"] = "バ", ["バ"] = "パ", ["パ"] = "ハ", + ["ヒ"] = "ビ", ["ビ"] = "ピ", ["ピ"] = "ヒ", + ["フ"] = "ブ", ["ブ"] = "プ", ["プ"] = "フ", + ["ヘ"] = "ベ", ["ベ"] = "ペ", ["ペ"] = "ヘ", + ["ホ"] = "ボ", ["ボ"] = "ポ", ["ポ"] = "ホ", + ["ヤ"] = "ャ", ["ャ"] = "ヤ", + ["ユ"] = "ュ", ["ュ"] = "ユ", + ["ヨ"] = "ョ", ["ョ"] = "ヨ", + ["ワ"] = "ヮ", ["ヮ"] = "ヷ", ["ヷ"] = "ワ", + ["ヲ"] = "ヺ", ["ヺ"] = "ヲ", + }, + [MODIFIER_DAKUTEN] = { + ["う"] = "ゔ", + ["か"] = "が", + ["き"] = "ぎ", + ["く"] = "ぐ", + ["け"] = "げ", + ["こ"] = "ご", + ["さ"] = "ざ", + ["し"] = "じ", + ["す"] = "ず", + ["せ"] = "ぜ", + ["そ"] = "ぞ", + ["た"] = "だ", + ["ち"] = "ぢ", + ["つ"] = "づ", + ["て"] = "で", + ["と"] = "ど", + ["は"] = "ば", + ["ひ"] = "び", + ["ふ"] = "ぶ", + ["へ"] = "べ", + ["ほ"] = "ぼ", + ["ウ"] = "ヴ", + ["カ"] = "ガ", + ["キ"] = "ギ", + ["ク"] = "グ", + ["ケ"] = "ゲ", + ["コ"] = "ゴ", + ["サ"] = "ザ", + ["シ"] = "ジ", + ["ス"] = "ズ", + ["セ"] = "ゼ", + ["ソ"] = "ゾ", + ["タ"] = "ダ", + ["チ"] = "ヂ", + ["ツ"] = "ヅ", + ["テ"] = "デ", + ["ト"] = "ド", + ["ハ"] = "バ", + ["ヒ"] = "ビ", + ["フ"] = "ブ", + ["ヘ"] = "ベ", + ["ホ"] = "ボ", + ["ワ"] = "ヷ", + ["ヲ"] = "ヺ", + ["ぱ"] = "ば", + ["ぴ"] = "び", + ["ぷ"] = "ぶ", + ["ぺ"] = "べ", + ["ぽ"] = "ぼ", + ["パ"] = "バ", + ["ピ"] = "ビ", + ["プ"] = "ブ", + ["ペ"] = "ベ", + ["ポ"] = "ボ", + ["ぅ"] = "ゔ", + ["っ"] = "づ", + ["ゥ"] = "ヴ", + ["ヮ"] = "ヷ", + ["ッ"] = "ヅ", + }, + [MODIFIER_HANDAKUTEN] = { + ["は"] = "ぱ", + ["ひ"] = "ぴ", + ["ふ"] = "ぷ", + ["へ"] = "ぺ", + ["ほ"] = "ぽ", + ["ハ"] = "パ", + ["ヒ"] = "ピ", + ["フ"] = "プ", + ["ヘ"] = "ペ", + ["ホ"] = "ポ", + ["ば"] = "ぱ", + ["び"] = "ぴ", + ["ぶ"] = "ぷ", + ["べ"] = "ぺ", + ["ぼ"] = "ぽ", + ["バ"] = "パ", + ["ビ"] = "ピ", + ["ブ"] = "プ", + ["ベ"] = "ペ", + ["ボ"] = "ポ", + }, + [MODIFIER_SMALLKANA] = { + ["あ"] = "ぁ", + ["い"] = "ぃ", + ["え"] = "ぇ", + ["う"] = "ぅ", + ["お"] = "ぉ", + ["つ"] = "っ", + ["や"] = "ゃ", + ["ゆ"] = "ゅ", + ["よ"] = "ょ", + ["わ"] = "ゎ", + ["ア"] = "ァ", + ["イ"] = "ィ", + ["ウ"] = "ゥ", + ["エ"] = "ェ", + ["オ"] = "ォ", + ["ツ"] = "ッ", + ["ヤ"] = "ャ", + ["ユ"] = "ュ", + ["ヨ"] = "ョ", + ["ワ"] = "ヮ", + ["ゔ"] = "ぅ", + ["づ"] = "っ", + ["ヴ"] = "ゥ", + ["ヅ"] = "ッ", + ["ヷ"] = "ヮ", + }, + [MODIFIER_SHIFT] = { + ["a"] = "A", ["A"] = "a", + ["b"] = "B", ["B"] = "b", + ["c"] = "C", ["C"] = "c", + ["d"] = "D", ["D"] = "d", + ["e"] = "E", ["E"] = "e", + ["f"] = "F", ["F"] = "f", + ["g"] = "G", ["G"] = "g", + ["h"] = "H", ["H"] = "h", + ["i"] = "I", ["I"] = "i", + ["j"] = "J", ["J"] = "j", + ["k"] = "K", ["K"] = "k", + ["l"] = "L", ["L"] = "l", + ["m"] = "M", ["M"] = "m", + ["n"] = "N", ["N"] = "n", + ["o"] = "O", ["O"] = "o", + ["p"] = "P", ["P"] = "p", + ["q"] = "Q", ["Q"] = "q", + ["r"] = "R", ["R"] = "r", + ["s"] = "S", ["S"] = "s", + ["t"] = "T", ["T"] = "t", + ["u"] = "U", ["U"] = "u", + ["v"] = "V", ["V"] = "v", + ["w"] = "W", ["W"] = "w", + ["x"] = "X", ["X"] = "x", + ["y"] = "Y", ["Y"] = "y", + ["z"] = "Z", ["Z"] = "z", + }, + }, +} diff --git a/tools/ja_keyboard_generate.py b/tools/ja_keyboard_generate.py new file mode 100755 index 000000000..576510a31 --- /dev/null +++ b/tools/ja_keyboard_generate.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2021 Aleksa Sarai +# Licensed under the AGPLv3-or-later. +# +# usage: ./tools/ja_keyboard_generate.py > frontend/ui/data/keyboardlayouts/ja_keyboard_keys.lua +# +# Generates the modifier cycle table for the Japanese 12-key flick keyboard as +# well as the cycle table for each key, the goal being to create an efficient +# mapping for each kana so that when a given modifier is pressed we can easily +# switch to the next key. Each kana is part of a cycle so pressing the modifier +# key multiple times will loop through the options, as will tapping the same +# letter multiple times. + +import os +import unicodedata + +import jinja2 + +def NFC(s): return unicodedata.normalize("NFC", s) +def NFD(s): return unicodedata.normalize("NFD", s) + +RAW_DAKUTEN = "\u3099" +RAW_HANDAKUTEN = "\u309A" + +def modified_kana(kana): + # Try to produce versions of the kana which are combined with dakuten or + # handakuten. We only care about combined versions of the character if the + # combined version is a single codepoint (which means it's a "standard" + # combination and is thus a valid modified version of the given kana). + # + # Python3's len() counts the number of codepoints, which is what we want. + return [ NFC(kana+modifier) + for modifier in [RAW_DAKUTEN, RAW_HANDAKUTEN] + if len(NFC(kana+modifier)) == 1 ] + +# Hiragana and katakana without any dakuten. +BASE_KANA = "あいえうおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん" + \ + "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン" + +# The set of small kana (from their big kana equivalent). +TO_SMALL_KANA = { + # Hiragana + "あ": "ぁ", "い": "ぃ", "う": "ぅ", "え": "ぇ", "お": "ぉ", + "や": "ゃ", "ゆ": "ゅ", "よ": "ょ", + "わ": "ゎ", "つ": "っ", + + # Katakana + "ア": "ァ", "イ": "ィ", "ウ": "ゥ", "エ": "ェ", "オ": "ォ", + "ヤ": "ャ", "ユ": "ュ", "ヨ": "ョ", + "ワ": "ヮ", "ツ": "ッ", +} +# ... and vice-versa. +FROM_SMALL_KANA = {small: big for big, small in TO_SMALL_KANA.items()} + +# The set of kana derived from BASE_KANA. +MODIFIED_KANA = "".join("".join(modified_kana(kana)) for kana in BASE_KANA) +SMALL_KANA = "".join(FROM_SMALL_KANA.keys()) +ALL_KANA = BASE_KANA + MODIFIED_KANA + SMALL_KANA + +EN_ALPHABET = "abcdefghijklmnopqrstuvwxyz" + +def escape_luastring(s): + # We cannot use repr() because Python escapes are not valid Lua. + return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"' + +def generate_cycle(kana): + "Generate an array describing the modifier cycle for a given kana." + # The cycle starts with the provided kana. + cycle = [NFC(kana)] + # If there are any small kana, add them to the cycle. + if kana in TO_SMALL_KANA: + cycle.append(TO_SMALL_KANA[kana]) + # If there are any valid modifications of this kana, add them to the cycle. + cycle.extend(modified_kana(kana)) + return cycle + +def generate_basic_cycle(kana, modifier): + """ + Generate an array describing a basic cycle using just the given combining + mark. + """ + cycle = [NFC(kana)] + # Remove any combining marks and convert back to large kana if possible. + # This allows us to create cycles which start with a modified kana (mainly + # useful for the dedicated modifiers). + base_kana, *_ = NFD(kana) + if base_kana in FROM_SMALL_KANA: + base_kana = FROM_SMALL_KANA[base_kana] + new_kana = NFC(base_kana + modifier) + if new_kana != kana and len(new_kana) == 1: + cycle.append(new_kana) + return cycle + +def generate_smallkana_cycle(kana): + cycle = [NFC(kana)] + base_kana, *_ = NFD(kana) # Remove any combining marks. + if base_kana in TO_SMALL_KANA: + cycle.append(TO_SMALL_KANA[base_kana]) + return cycle + +def generate_alphabet_shift_cycle(letter): + return [letter, letter.upper()] + +def output_cycle(cycle, loop=True): + "Return a snippet of a Lua table describing the cycle passed." + cycle = list(cycle) + if len(cycle) == 1: + return "" # Don't do anything for noop one-kana cycles. + if loop: + # Map the last kana back to the start. + mapping = zip(cycle, cycle[1:] + [cycle[0]]) + else: + # The last kana doesn't get any mapping. + mapping = zip(cycle, cycle[1:]) + lua_snippet = [] + for kana_src, kana_dst in mapping: + lua_snippet.append(f"[{escape_luastring(kana_src)}] = {escape_luastring(kana_dst)},") + return " ".join(lua_snippet) + +# Straightforward cycle over all options. +cyclic_table = [ output_cycle(generate_cycle(kana)) for kana in BASE_KANA ] + +# For all of the specialised tables we do not loop back to the original kana. +# This is done to match the GBoard behaviour, where only the base 変換 button +# loops back through all of the options. +dakuten_table = [ output_cycle(generate_basic_cycle(kana, RAW_DAKUTEN), loop=False) for kana in ALL_KANA ] +handakuten_table = [ output_cycle(generate_basic_cycle(kana, RAW_HANDAKUTEN), loop=False) for kana in ALL_KANA ] +smallkana_table = [ output_cycle(generate_smallkana_cycle(kana), loop=False) for kana in ALL_KANA ] +# NOTE: If we ever want to enable looping for these modifiers, just set +# loop=True for BASE_KANA and loop=False only for the derived +# {MODIFIED,SMALL}_KANA. + +# Straightforward cycle through shifted and unshifted letters. +shift_table = [ output_cycle(generate_alphabet_shift_cycle(letter)) for letter in EN_ALPHABET ] + +class Key(object): + def __init__(self, name, popout, loop=None, label=None, alt_label=None): + self.name = name + self.popout = popout + self.label = label + self.alt_label = alt_label + self.loop = loop or popout # default to popout order + + def render_key(self, indent_level=1): + lua_items = [] + if self.label: + lua_items.append(f'label = {escape_luastring(self.label)}') + if self.alt_label: + lua_items.append(f'alt_label = {escape_luastring(self.alt_label)}') + if lua_items: + lua_items.append("\n") # Put the labels on a separate line. + for direction, key in zip(["", "west", "north", "east", "south"], self.popout): + if direction: + lua_items.append(f'{direction} = {escape_luastring(key)}') + else: + lua_items.append(f'{escape_luastring(key)}') + lua_item = f'{self.name} = {{ {", ".join(lua_items)} }}' + # Fix newlines to match the indentation and remove the doubled comma. + indent = len(self.name) + 4 * (indent_level + 1) + return lua_item.replace(", \n, ", ",\n" + " " * indent) + + def render_key_cycle(self): + cycle = output_cycle(self.loop) + if cycle: + return f"{{ {cycle} }}" + else: + return "nil" + +# Hiragana, katakana, latin, and symbol keys in [tap, east, north, west, south] +# order to match GBoard/Flick input. This is basically the Japanese version of +# T9 order. The keys are the variable names we assign for each keypad, for use +# in ja_keyboard.lua. +KEYPADS = [ + # Hiragana keys. + Key("h_a", "あいうえお", loop="あいうえおぁぃぅぇぉ"), + Key("hKa", "かきくけこ"), + Key("hSa", "さしすせそ"), + Key("hTa", "たちつてと", loop="たちつてとっ"), + Key("hNa", "なにぬねの"), + Key("hHa", "はひふへほ"), + Key("hMa", "まみむめも"), + Key("hYa", "や(ゆ)よ", loop="やゆよゃゅょ", alt_label="()"), + Key("hRa", "らりるれろ"), + Key("hWa", "わをんー〜", loop="わをんゎー〜", alt_label="ー〜"), + Key("h_P", "、。?!…", loop="、。?!…・ ", alt_label="。?!…"), + + # Katakana keys. + Key("k_a", "アイウエオ", loop="アイウエオァィゥェォ"), + Key("kKa", "カキクケコ"), + Key("kSa", "サシスセソ"), + Key("kTa", "タチツテト", loop="タチツテトッ"), + Key("kNa", "ナニヌネノ"), + Key("kHa", "ハヒフヘホ"), + Key("kMa", "マミムメモ"), + Key("kYa", "ヤ(ユ)ヨ", loop="ヤユヨャュョ", alt_label="()"), + Key("kRa", "ラリルレロ"), + Key("kWa", "ワヲンー〜", loop="ワヲンヮー〜", alt_label="ー〜"), + Key("k_P", "、。?!…", loop="、。?!…・ ", alt_label="。?!…"), + + # Latin alphabet. + Key("l_1", "@-_/1", label="@-_/", alt_label="1"), + Key("l_2", "abc2", loop="abcABC2", label="abc", alt_label="2"), + Key("l_3", "def3", loop="defDEF3", label="def", alt_label="3"), + Key("l_4", "ghi4", loop="ghiGHI4", label="ghi", alt_label="4"), + Key("l_5", "jkl5", loop="jklJKL5", label="jkl", alt_label="5"), + Key("l_6", "mno6", loop="mnoMNO6", label="mno", alt_label="6"), + Key("l_7", "pqrs7", loop="pqrsPQRS7", label="pqrs", alt_label="7"), + Key("l_8", "tuv8", loop="tuvTUV8", label="tuv", alt_label="8"), + Key("l_9", "wxyz9", loop="wxyzWXYZ9", label="wxyz", alt_label="9"), + Key("l_0", "'\":;0", label="'\":;", alt_label="0"), + Key("l_P", ",.?!", label=",.?!"), + + # Symbol / numpad keys. Note that we do not have any loops for this layer. + Key("s_1", "1☆♪", loop="1", alt_label="☆♪"), # NOTE: Cannot include → because it's used internally. + Key("s_2", "2¥$€", loop="2", alt_label="¥$€"), + Key("s_3", "3%゜#", loop="3", alt_label="%゜#"), + Key("s_4", "4○*・", loop="4", alt_label="○*・"), + Key("s_5", "5+×÷", loop="5", alt_label="+×÷"), + Key("s_6", "6<=>", loop="6", alt_label="<=>"), + Key("s_7", "7「」:", loop="7", alt_label="「」:"), + Key("s_8", "8〒々〆", loop="8", alt_label="〒々〆"), + Key("s_9", "9^|\\", loop="9", alt_label="^|\\"), + Key("s_0", "0~…@", loop="0", alt_label="~…@"), + Key("s_b", "()[]", loop="(", label="()[]"), + Key("s_p", ".,-/", loop=".", label=".,-/"), +] + +TEMPLATE = jinja2.Template(""" +--- @note This file was generated with tools/ja_keyboard_generate.py. +-- DO NOT EDIT THIS FILE MANUALLY. Instead, edit and re-run the script. + +-- These values are displayed to users when they long-press on the modifier +-- key, so make them somewhat understandable (変換 is not the best word to use +-- for the cycle button because it's fairly generic and in IMEs it usually +-- indicates cycling through the IME suggestions but I couldn't find any +-- documentation about the 12-key keyboard that uses a more specific term). + +local MODIFIER_CYCLIC = "変換" +local MODIFIER_DAKUTEN = "◌゙" +local MODIFIER_HANDAKUTEN = "◌゚" +local MODIFIER_SMALLKANA = "小" +local MODIFIER_SHIFT = "\uED35" + +return { + -- Keypad definitions. +{% for key in KEYPADS %} + {{ key.render_key() }}, +{% endfor %} + + -- Cycle lookup table for keitai (multi-tap) keypad input. + KEITAI_TABLE = { +{% for key in KEYPADS %} +{% set key_cycle = key.render_key_cycle() %} +{% if key_cycle != "nil" %} + {# + Some loops (including the trigger character) are repeated (mainly h_P + and k_P) but that's okay because the order is the same so we can just + output it once and skip the next one. + #} + {% set loop_id = key.popout[0] + key.loop %} + {% if loop_id not in seen_loops %} + ["{{ key.popout[0] }}"] = {{ key_cycle }}, + {# We need to do some trickery to do the "seen set" pattern in Jinja. #} + {{- [seen_loops.add(loop_id), ""][1] -}} + {% endif %} +{% endif %} +{% endfor %} + }, + + -- Special keycodes for the cyclic keys. + MODIFIER_KEY_CYCLIC = MODIFIER_CYCLIC, + MODIFIER_KEY_DAKUTEN = MODIFIER_DAKUTEN, + MODIFIER_KEY_HANDAKUTEN = MODIFIER_HANDAKUTEN, + MODIFIER_KEY_SMALLKANA = MODIFIER_SMALLKANA, + MODIFIER_KEY_SHIFT = MODIFIER_SHIFT, + + -- Modifier lookup table. + MODIFIER_TABLE = { + [MODIFIER_CYCLIC] = { +{% for entry in cyclic_table %} + {% if entry %} + {{ entry }} + {% endif %} +{% endfor %} + }, + [MODIFIER_DAKUTEN] = { +{% for entry in dakuten_table %} + {% if entry %} + {{ entry }} + {% endif %} +{% endfor %} + }, + [MODIFIER_HANDAKUTEN] = { +{% for entry in handakuten_table %} + {% if entry %} + {{ entry }} + {% endif %} +{% endfor %} + }, + [MODIFIER_SMALLKANA] = { +{% for entry in smallkana_table %} + {% if entry %} + {{ entry }} + {% endif %} +{% endfor %} + }, + [MODIFIER_SHIFT] = { +{% for entry in shift_table %} + {% if entry %} + {{ entry }} + {% endif %} +{% endfor %} + }, + }, +} +""", trim_blocks=True, lstrip_blocks=True) + +seen_loops = set() +print(TEMPLATE.render(locals()))