diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 844cc16..7b915ce 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,9 +13,11 @@ jobs: run: | export DEBIAN_FRONTEND=noninteractive apt-get update + apt install -y pkg-config apt install -y clang apt install -y cmake extra-cmake-modules gettext libfmt-dev apt install -y fcitx5 libfcitx5core-dev libfcitx5config-dev libfcitx5utils-dev + apt install -y libjson-c-dev - name: Build run: | mkdir -p build @@ -42,10 +44,11 @@ jobs: run: | export DEBIAN_FRONTEND=noninteractive apt-get update + apt install -y pkg-config apt install -y clang apt install -y cmake extra-cmake-modules gettext libfmt-dev apt install -y fcitx5 libfcitx5core-dev libfcitx5config-dev libfcitx5utils-dev - apt install -y libicu-dev + apt install -y libicu-dev libjson-c-dev - name: Build run: | mkdir -p build diff --git a/CMakeLists.txt b/CMakeLists.txt index 76a213e..0b7d3f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,11 +4,14 @@ project(fcitx5-mcbopomofo VERSION 2.5.2) find_package(ECM REQUIRED 1.0.0) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) +find_package(PkgConfig REQUIRED) find_package(Fcitx5Core REQUIRED) find_package(Fcitx5Utils REQUIRED) find_package(fmt REQUIRED) find_package(Gettext REQUIRED) +pkg_check_modules(JSONC REQUIRED IMPORTED_TARGET "json-c") + include(FeatureSummary) include(GNUInstallDirs) include(ECMSetupVersion) @@ -58,9 +61,11 @@ message(STATUS "CMAKE_INSTALL_DATADIR ${CMAKE_INSTALL_DATADIR}") configure_file(data/data.txt mcbopomofo-data.txt) configure_file(data/data-plain-bpmf.txt mcbopomofo-data-plain-bpmf.txt) configure_file(data/add-phrase-hook.sh mcbopomofo-add-phrase-hook.sh) +configure_file(data/dictionary_service.json mcbopomofo-dictionary-service.json) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/mcbopomofo-data.txt" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/data") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/mcbopomofo-data-plain-bpmf.txt" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/data") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/mcbopomofo-add-phrase-hook.sh" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/data") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/mcbopomofo-dictionary-service.json" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/data") fcitx5_translate_desktop_file(org.fcitx.Fcitx5.Addon.McBopomofo.metainfo.xml.in org.fcitx.Fcitx5.Addon.McBopomofo.metainfo.xml XML diff --git a/README.md b/README.md index 1d601a9..1056a61 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ ```bash sudo apt install \ - fcitx5 libfcitx5core-dev libfcitx5config-dev libfcitx5utils-dev \ - cmake extra-cmake-modules gettext libfmt-dev libicu-dev + pkg-config fcitx5 libfcitx5core-dev libfcitx5config-dev libfcitx5utils-dev \ + cmake extra-cmake-modules gettext libfmt-dev libicu-dev libjson-c-dev ``` 然後在本專案的 git 目錄下執行以下指令: diff --git a/build.sh b/build.sh index 8cd08f1..27fad38 100644 --- a/build.sh +++ b/build.sh @@ -1,7 +1,7 @@ #!/bin/sh -rm -rf build -mkdir -p build +# rm -rf build +# mkdir -p build cd build cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug # use Debug for easy debugging with gdb make # or ninja, depending on your system diff --git a/data/dictionary_service.json b/data/dictionary_service.json new file mode 100644 index 0000000..1c1b41f --- /dev/null +++ b/data/dictionary_service.json @@ -0,0 +1,56 @@ +{ + "services": [ + { + "name": "萌典", + "url_template": "https://www.moedict.tw/(encoded)" + }, + { + "name": "萌典 (台語)", + "url_template": "https://www.moedict.tw/'(encoded)" + }, + { + "name": "萌典 (客語)", + "url_template": "https://www.moedict.tw/:(encoded)" + }, + { + "name": "Google", + "url_template": "https://www.google.com/search?q=(encoded)" + }, + { + "name": "教育部重編國語詞典修訂本", + "url_template": "https://dict.revised.moe.edu.tw/search.jsp?md=1&word=(encoded)" + }, + { + "name": "教育部國語詞典簡編本", + "url_template": "https://dict.concised.moe.edu.tw/search.jsp?md=1&word=(encoded)" + }, + { + "name": "教育部成語典", + "url_template": "https://dict.idioms.moe.edu.tw/idiomList.jsp?idiom=(encoded)&qMd=0&qTp=1&qTp=2" + }, + { + "name": "教育部異體字字典", + "url_template": "https://dict.variants.moe.edu.tw/variants/rbt/query_result.do?from=standard&search_text=(encoded)" + }, + { + "name": "教育部國字標準字體筆順學習網", + "url_template": "https://stroke-order.learningweb.moe.edu.tw/charactersQueryResult.do?words=(encoded)&lang=zh_TW&csrfPreventionSalt=null" + }, + { + "name": "教育部臺灣閩南語常用詞辭典", + "url_template": "https://sutian.moe.edu.tw/zh-hant/tshiau/?lui=tai_su&tsha=(encoded)" + }, + { + "name": "Wiktionary", + "url_template": "https://zh.wiktionary.org/wiki/Special:Search?search=(encoded)" + }, + { + "name": "康熙字典網上版", + "url_template": "https://www.kangxizidian.com/search/index.php?stype=Word&sword=(encoded)&detail=n" + }, + { + "name": "Unihan Database", + "url_template": "https://www.unicode.org/cgi-bin/GetUnihanData.pl?codepoint=(encoded)" + } + ] +} diff --git a/po/en.po b/po/en.po index 8ef1971..a261951 100644 --- a/po/en.po +++ b/po/en.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: fcitx5-mcbopomofo v0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-30 10:34+0800\n" +"POT-Creation-Date: 2023-12-31 21:14+0800\n" "PO-Revision-Date: 2022-03-22 20:28-0700\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -17,83 +17,91 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/McBopomofo.cpp:129 +#: src/McBopomofo.cpp:164 msgid "Cursor is between syllables {0} and {1}" msgstr "" -#: src/McBopomofo.cpp:134 +#: src/McBopomofo.cpp:169 msgid "{0} syllables required" msgstr "" -#: src/McBopomofo.cpp:138 +#: src/McBopomofo.cpp:173 msgid "{0} syllables maximum" msgstr "" -#: src/McBopomofo.cpp:142 +#: src/McBopomofo.cpp:177 msgid "phrase already exists" msgstr "" -#: src/McBopomofo.cpp:146 +#: src/McBopomofo.cpp:181 msgid "press Enter to add the phrase" msgstr "" -#: src/McBopomofo.cpp:152 +#: src/McBopomofo.cpp:187 msgid "Marked: {0}, syllables: {1}, {2}" msgstr "" -#: src/McBopomofo.cpp:164 +#: src/McBopomofo.cpp:199 msgid "# Custom Phrases or Characters." msgstr "" -#: src/McBopomofo.cpp:166 +#: src/McBopomofo.cpp:201 msgid "" "# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動加詞 for " "usage." msgstr "" -#: src/McBopomofo.cpp:168 +#: src/McBopomofo.cpp:203 msgid "" "# Add your phrases and their respective Bopomofo reading below. Use hyphen " "(\"-\")" msgstr "" -#: src/McBopomofo.cpp:169 +#: src/McBopomofo.cpp:204 msgid "# to connect the Bopomofo syllables." msgstr "" -#: src/McBopomofo.cpp:173 src/McBopomofo.cpp:192 +#: src/McBopomofo.cpp:208 src/McBopomofo.cpp:227 msgid "# Any line that starts with \"#\" is treated as comment." msgstr "" -#: src/McBopomofo.cpp:182 +#: src/McBopomofo.cpp:217 msgid "# Custom Excluded Phrases or Characters." msgstr "" -#: src/McBopomofo.cpp:184 +#: src/McBopomofo.cpp:219 msgid "" "# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞 for " "usage." msgstr "" -#: src/McBopomofo.cpp:186 +#: src/McBopomofo.cpp:221 msgid "" "# For example, the line below will prevent the phrase \"家祠\" from showing " "up anywhere:" msgstr "" -#: src/McBopomofo.cpp:190 +#: src/McBopomofo.cpp:225 msgid "" "# Note that you need to use a hyphen (\"-\") between Bopomofo syllables." msgstr "" -#: src/McBopomofo.cpp:231 +#: src/McBopomofo.cpp:266 msgid "Edit User Phrases" msgstr "" -#: src/McBopomofo.cpp:241 +#: src/McBopomofo.cpp:276 msgid "Edit Excluded Phrases" msgstr "" +#: src/McBopomofo.cpp:876 +msgid "UTF8 String Length: {0}" +msgstr "" + +#: src/McBopomofo.cpp:879 +msgid "Code Point Count: {0}" +msgstr "" + #: src/McBopomofo.h:59 msgid "standard" msgstr "Standard" @@ -172,15 +180,15 @@ msgstr "Input HTML Ruby Text" #: src/McBopomofo.h:98 msgid "Bopomofo Keyboard Layout" -msgstr "" +msgstr "Bopomofo Keyboard Layout" #: src/McBopomofo.h:104 msgid "Candidate List Layout" -msgstr "" +msgstr "Candidate List Layout" #: src/McBopomofo.h:109 msgid "Selection Keys" -msgstr "" +msgstr "Selection Keys" #: src/McBopomofo.h:114 msgid "Show Candidate Phrase" @@ -188,30 +196,38 @@ msgstr "Show Candidates" #: src/McBopomofo.h:119 msgid "Move cursor after selection" -msgstr "" +msgstr "Move cursor after selection" #: src/McBopomofo.h:125 msgid "ESC key clears entire composing buffer" -msgstr "" +msgstr "ESC key clears entire composing buffer" #: src/McBopomofo.h:129 msgid "Shift + Letter Keys" -msgstr "" +msgstr "Shift + Letter Keys" #: src/McBopomofo.h:135 msgid "Control + Enter Key" -msgstr "" +msgstr "Control + Enter Key" #: src/McBopomofo.h:140 msgid "Open User Phrase Files With" -msgstr "" +msgstr "Open User Phrase Files With" #: src/McBopomofo.h:145 msgid "Add Phrase Hook Path" -msgstr "" +msgstr "Add Phrase Hook Path" #: src/McBopomofo.h:150 msgid "Run the hook script after adding a phrase" +msgstr "Run the hook script after adding a phrase" + +#: src/DictionaryService.cpp:37 src/DictionaryService.cpp:55 +msgid "Character Information" +msgstr "" + +#: src/DictionaryService.cpp:76 +msgid "Look up \"{0}\" in {1}" msgstr "" #: src/mcbopomofo.conf.in.in:3 src/mcbopomofo-addon.conf.in.in:3 @@ -225,13 +241,16 @@ msgstr "Plain Bopomofo" #: src/mcbopomofo-addon.conf.in.in:4 msgid "McBopomofo Input Method For Fcitx" -msgstr "" +msgstr "McBopomofo Input Method For Fcitx" #: org.fcitx.Fcitx5.Addon.McBopomofo.metainfo.xml.in:7 #, fuzzy msgid "McBopomofo for Fcitx 5" -msgstr "Plain Bopomofo" +msgstr "McBopomofo for Fcitx 5" #: org.fcitx.Fcitx5.Addon.McBopomofo.metainfo.xml.in:8 msgid "A Bopomofo input method that picks phrases intelligently" -msgstr "" +msgstr "A Bopomofo input method that picks phrases intelligently" + +#~ msgid "Open URL With" +#~ msgstr "Open URL With" diff --git a/po/fcitx5-mcbopomofo.pot b/po/fcitx5-mcbopomofo.pot index 374ac4f..2e11979 100644 --- a/po/fcitx5-mcbopomofo.pot +++ b/po/fcitx5-mcbopomofo.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: fcitx5-mcbopomofo\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-30 10:34+0800\n" +"POT-Creation-Date: 2023-12-31 21:14+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,83 +17,91 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/McBopomofo.cpp:129 +#: src/McBopomofo.cpp:164 msgid "Cursor is between syllables {0} and {1}" msgstr "" -#: src/McBopomofo.cpp:134 +#: src/McBopomofo.cpp:169 msgid "{0} syllables required" msgstr "" -#: src/McBopomofo.cpp:138 +#: src/McBopomofo.cpp:173 msgid "{0} syllables maximum" msgstr "" -#: src/McBopomofo.cpp:142 +#: src/McBopomofo.cpp:177 msgid "phrase already exists" msgstr "" -#: src/McBopomofo.cpp:146 +#: src/McBopomofo.cpp:181 msgid "press Enter to add the phrase" msgstr "" -#: src/McBopomofo.cpp:152 +#: src/McBopomofo.cpp:187 msgid "Marked: {0}, syllables: {1}, {2}" msgstr "" -#: src/McBopomofo.cpp:164 +#: src/McBopomofo.cpp:199 msgid "# Custom Phrases or Characters." msgstr "" -#: src/McBopomofo.cpp:166 +#: src/McBopomofo.cpp:201 msgid "" "# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動加詞 for " "usage." msgstr "" -#: src/McBopomofo.cpp:168 +#: src/McBopomofo.cpp:203 msgid "" "# Add your phrases and their respective Bopomofo reading below. Use hyphen " "(\"-\")" msgstr "" -#: src/McBopomofo.cpp:169 +#: src/McBopomofo.cpp:204 msgid "# to connect the Bopomofo syllables." msgstr "" -#: src/McBopomofo.cpp:173 src/McBopomofo.cpp:192 +#: src/McBopomofo.cpp:208 src/McBopomofo.cpp:227 msgid "# Any line that starts with \"#\" is treated as comment." msgstr "" -#: src/McBopomofo.cpp:182 +#: src/McBopomofo.cpp:217 msgid "# Custom Excluded Phrases or Characters." msgstr "" -#: src/McBopomofo.cpp:184 +#: src/McBopomofo.cpp:219 msgid "" "# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞 for " "usage." msgstr "" -#: src/McBopomofo.cpp:186 +#: src/McBopomofo.cpp:221 msgid "" "# For example, the line below will prevent the phrase \"家祠\" from showing " "up anywhere:" msgstr "" -#: src/McBopomofo.cpp:190 +#: src/McBopomofo.cpp:225 msgid "" "# Note that you need to use a hyphen (\"-\") between Bopomofo syllables." msgstr "" -#: src/McBopomofo.cpp:231 +#: src/McBopomofo.cpp:266 msgid "Edit User Phrases" msgstr "" -#: src/McBopomofo.cpp:241 +#: src/McBopomofo.cpp:276 msgid "Edit Excluded Phrases" msgstr "" +#: src/McBopomofo.cpp:876 +msgid "UTF8 String Length: {0}" +msgstr "" + +#: src/McBopomofo.cpp:879 +msgid "Code Point Count: {0}" +msgstr "" + #: src/McBopomofo.h:59 msgid "standard" msgstr "" @@ -214,6 +222,14 @@ msgstr "" msgid "Run the hook script after adding a phrase" msgstr "" +#: src/DictionaryService.cpp:37 src/DictionaryService.cpp:55 +msgid "Character Information" +msgstr "" + +#: src/DictionaryService.cpp:76 +msgid "Look up \"{0}\" in {1}" +msgstr "" + #: src/mcbopomofo.conf.in.in:3 src/mcbopomofo-addon.conf.in.in:3 msgid "McBopomofo" msgstr "" diff --git a/po/zh_TW.po b/po/zh_TW.po index 48ffa77..c517dc7 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: fcitx5-mcbopomofo v0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-30 10:34+0800\n" +"POT-Creation-Date: 2023-12-31 21:14+0800\n" "PO-Revision-Date: 2022-12-28 21:39+0800\n" "Last-Translator: Chaoting Liu \n" "Language-Team: none\n" @@ -18,85 +18,98 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 3.2.2\n" -#: src/McBopomofo.cpp:129 +#: src/McBopomofo.cpp:164 msgid "Cursor is between syllables {0} and {1}" msgstr "游標在「{0}」與「{1}」之間" -#: src/McBopomofo.cpp:134 +#: src/McBopomofo.cpp:169 msgid "{0} syllables required" msgstr "至少需要選取{0}個字" -#: src/McBopomofo.cpp:138 +#: src/McBopomofo.cpp:173 msgid "{0} syllables maximum" msgstr "最多只能選取{0}個字" -#: src/McBopomofo.cpp:142 +#: src/McBopomofo.cpp:177 msgid "phrase already exists" msgstr "詞庫已經有這個詞" -#: src/McBopomofo.cpp:146 +#: src/McBopomofo.cpp:181 msgid "press Enter to add the phrase" msgstr "請按 Enter 加入自訂詞庫" -#: src/McBopomofo.cpp:152 +#: src/McBopomofo.cpp:187 msgid "Marked: {0}, syllables: {1}, {2}" msgstr "選取了「{0}」,注音「{1}」:{2}" -#: src/McBopomofo.cpp:164 +#: src/McBopomofo.cpp:199 msgid "# Custom Phrases or Characters." msgstr "# 手動加詞資料檔" -#: src/McBopomofo.cpp:166 +#: src/McBopomofo.cpp:201 msgid "" -"# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動加詞 for usage." +"# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動加詞 for " +"usage." msgstr "" -"# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動加詞" +"# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動" +"加詞" -#: src/McBopomofo.cpp:168 +#: src/McBopomofo.cpp:203 msgid "" "# Add your phrases and their respective Bopomofo reading below. Use hyphen " "(\"-\")" msgstr "" "# 請在下方加入用戶自訂字詞。每個詞後面要有字詞的讀音。注音音節之間要用減" -#: src/McBopomofo.cpp:169 +#: src/McBopomofo.cpp:204 msgid "# to connect the Bopomofo syllables." msgstr "# 號 (\"-\") 分隔。例如,以下範例加入「小麥注音」一詞:" -#: src/McBopomofo.cpp:173 src/McBopomofo.cpp:192 +#: src/McBopomofo.cpp:208 src/McBopomofo.cpp:227 msgid "# Any line that starts with \"#\" is treated as comment." msgstr "# 如果任何一行以 \"#\" 開頭,該行將被當作註解忽略。" -#: src/McBopomofo.cpp:182 +#: src/McBopomofo.cpp:217 msgid "# Custom Excluded Phrases or Characters." msgstr "# 手動刪詞資料檔" -#: src/McBopomofo.cpp:184 +#: src/McBopomofo.cpp:219 msgid "" -"# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞 for usage." +"# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞 for " +"usage." msgstr "" -"# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞" +"# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動" +"刪詞" -#: src/McBopomofo.cpp:186 +#: src/McBopomofo.cpp:221 msgid "" "# For example, the line below will prevent the phrase \"家祠\" from showing " "up anywhere:" msgstr "" -"# 如果將下面這行範例加入資料檔中,輸入 \"ㄐㄧㄚ ㄘˊ\" 時不會再看到「家祠」一詞:" +"# 如果將下面這行範例加入資料檔中,輸入 \"ㄐㄧㄚ ㄘˊ\" 時不會再看到「家祠」一" +"詞:" -#: src/McBopomofo.cpp:190 +#: src/McBopomofo.cpp:225 msgid "" "# Note that you need to use a hyphen (\"-\") between Bopomofo syllables." msgstr "# 請注意,注音音節之間要用減號 (\"-\") 分隔。" -#: src/McBopomofo.cpp:231 +#: src/McBopomofo.cpp:266 msgid "Edit User Phrases" msgstr "編輯使用者詞彙" -#: src/McBopomofo.cpp:241 +#: src/McBopomofo.cpp:276 msgid "Edit Excluded Phrases" msgstr "編輯排除的詞彙" +#: src/McBopomofo.cpp:876 +msgid "UTF8 String Length: {0}" +msgstr "UTF8 字串長度 {0}" + +#: src/McBopomofo.cpp:879 +msgid "Code Point Count: {0}" +msgstr "總字數 {0}" + #: src/McBopomofo.h:59 msgid "standard" msgstr "標準" @@ -217,6 +230,14 @@ msgstr "加詞腳本路徑" msgid "Run the hook script after adding a phrase" msgstr "在加入新詞之後執行加詞腳本" +#: src/DictionaryService.cpp:37 src/DictionaryService.cpp:55 +msgid "Character Information" +msgstr "字元資訊" + +#: src/DictionaryService.cpp:76 +msgid "Look up \"{0}\" in {1}" +msgstr "在{1}尋找「{0}」" + #: src/mcbopomofo.conf.in.in:3 src/mcbopomofo-addon.conf.in.in:3 msgid "McBopomofo" msgstr "小麥注音" @@ -236,3 +257,6 @@ msgstr "Fctix 5 的小麥注音" #: org.fcitx.Fcitx5.Addon.McBopomofo.metainfo.xml.in:8 msgid "A Bopomofo input method that picks phrases intelligently" msgstr "一套智慧選字的注音輸入法" + +#~ msgid "Open URL With" +#~ msgstr "開啟網址連結時要用" diff --git a/run-xgettext.sh b/run-xgettext.sh index cd76d0d..940ef38 100644 --- a/run-xgettext.sh +++ b/run-xgettext.sh @@ -7,7 +7,7 @@ xgettext \ -k_ \ -kN_ \ -o po/fcitx5-mcbopomofo.pot \ - src/McBopomofo.cpp src/McBopomofo.h + src/McBopomofo.cpp src/McBopomofo.h src/DictionaryService.cpp xgettext \ --package-name=fcitx5-mcbopomofo \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5f9b466..2b91ab2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,11 +1,17 @@ cmake_minimum_required(VERSION 3.6) project(mcbopomofo VERSION 2.5.2) +find_package(PkgConfig REQUIRED) find_package(Fcitx5Core REQUIRED) find_package(Fcitx5Utils REQUIRED) find_package(fmt REQUIRED) find_package(Gettext REQUIRED) find_package(ICU COMPONENTS uc i18n REQUIRED) + +pkg_check_modules(JSONC REQUIRED IMPORTED_TARGET "json-c") +include_directories(${JSONC_INCLUDE_DIRS}) +link_directories(${JSONC_LIBRARY_DIRS}) + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.23.0") find_package(GTest) endif() @@ -17,7 +23,9 @@ set(MCBOPOMOFO_LIB_SOURCES LanguageModelLoader.cpp UTF8Helper.cpp InputMacro.cpp - Log.cpp) + Log.cpp + DictionaryService.cpp +) # https://stackoverflow.com/questions/26549137/shared-library-on-linux-and-fpic-error set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") @@ -27,7 +35,7 @@ include("${FCITX_INSTALL_CMAKECONFIG_DIR}/Fcitx5Utils/Fcitx5CompilerSettings.cma add_library(McBopomofoLib ${MCBOPOMOFO_LIB_SOURCES}) target_compile_options(McBopomofoLib PRIVATE -Wno-unknown-pragmas) -target_link_libraries(McBopomofoLib PRIVATE Fcitx5::Utils ICU::uc ICU::i18n gramambular2_lib McBopomofoLMLib MandarinLib) +target_link_libraries(McBopomofoLib PRIVATE Fcitx5::Utils ICU::uc ICU::i18n ${JSONC_LIBRARIES} gramambular2_lib McBopomofoLMLib MandarinLib) target_include_directories(McBopomofoLib PRIVATE Fcitx5::Utils) target_compile_definitions(McBopomofoLib PRIVATE FCITX_GETTEXT_DOMAIN=\"fcitx5-mcbopomofo\") @@ -46,7 +54,7 @@ if (USE_LEGACY_FCITX5_API) add_compile_definitions(mcbopomofo USE_LEGACY_FCITX5_API=1) endif() target_compile_options(mcbopomofo PRIVATE -Wno-unknown-pragmas) -target_link_libraries(mcbopomofo PRIVATE Fcitx5::Core McBopomofoLib fmt::fmt) +target_link_libraries(mcbopomofo PRIVATE Fcitx5::Core McBopomofoLib fmt::fmt ${JSONC_LIBRARIES}) target_include_directories(mcbopomofo PRIVATE Fcitx5::Core fmt::fmt) set_target_properties(mcbopomofo PROPERTIES PREFIX "") target_compile_definitions(mcbopomofo PRIVATE FCITX_GETTEXT_DOMAIN=\"fcitx5-mcbopomofo\") @@ -97,8 +105,8 @@ if (ENABLE_TEST) add_executable(McBopomofoTest KeyHandlerTest.cpp UTF8HelperTest.cpp) target_compile_options(McBopomofoTest PRIVATE -Wno-unknown-pragmas) - target_link_libraries(McBopomofoTest PRIVATE Fcitx5::Core GTest::gtest_main GTest::gmock_main McBopomofoLib) - target_include_directories(McBopomofoTest PRIVATE Fcitx5::Core) + target_link_libraries(McBopomofoTest PRIVATE Fcitx5::Core GTest::gtest_main GTest::gmock_main McBopomofoLib fmt::fmt ${JSONC_LIBRARIES}) + target_include_directories(McBopomofoTest PRIVATE Fcitx5::Core fmt::fmt) configure_file(../data/data.txt mcbopomofo-test-data.txt) diff --git a/src/DictionaryService.cpp b/src/DictionaryService.cpp new file mode 100644 index 0000000..a604a36 --- /dev/null +++ b/src/DictionaryService.cpp @@ -0,0 +1,154 @@ +#include "DictionaryService.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "Log.h" + +constexpr char kDataPath[] = "data/mcbopomofo-dictionary-service.json"; + +std::string urlEncode(const std::string& str) { + std::ostringstream encodedStream; + encodedStream << std::hex << std::uppercase << std::setfill('0'); + + for (char c : str) { + if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { + encodedStream << c; + } else { + encodedStream << '%' << std::setw(2) + << static_cast(static_cast(c)); + } + } + + return encodedStream.str(); +} + +class CharacterInfoService : public McBopomofo::DictionaryService { + public: + std::string name() const override { + return fmt::format(_("Character Information")); + } + + void lookup(std::string phrase, McBopomofo::InputState* state, + size_t /*Unused*/, + const McBopomofo::StateCallback& stateCallback) override { + auto selecting = + dynamic_cast(state); + if (selecting != nullptr) { + auto copy = selecting->copy(); + auto newState = + std::make_unique( + std::move(copy), phrase); + stateCallback(std::move(newState)); + } + } + + std::string textForMenu(std::string /*Unused*/) const override { + return fmt::format(_("Character Information")); + } +}; + +class HttpBasedDictionaryService : public McBopomofo::DictionaryService { + public: + HttpBasedDictionaryService(std::string name, std::string urlTemplate) + : name_(std::move(name)), urlTemplate_(std::move(urlTemplate)) {} + ~HttpBasedDictionaryService() override = default; + std::string name() const override { return name_; }; + + void lookup(std::string phrase, McBopomofo::InputState* /*unused*/, + size_t /*unused*/, + const McBopomofo::StateCallback& /*unused*/) override { + std::string url = urlTemplate_; + std::string encoded = "(encoded)"; + url.replace(url.find(encoded), url.length(), urlEncode(phrase)); + fcitx::startProcess({"xdg-open", url}); + } + + std::string textForMenu(std::string selectedString) const override { + return fmt::format(_("Look up \"{0}\" in {1}"), selectedString, name_); + } + + private: + std::string name_; + std::string urlTemplate_; +}; + +McBopomofo::DictionaryServices::DictionaryServices() = default; + +bool McBopomofo::DictionaryServices::hasServices() { + return !services_.empty(); +} + +void McBopomofo::DictionaryServices::lookup( + std::string phrase, size_t serviceIndex, InputState* state, + const StateCallback& stateCallback) { + if (serviceIndex >= services_.size()) { + return; + } + auto service = services_[serviceIndex].get(); + service->lookup(std::move(phrase), state, serviceIndex, stateCallback); +} + +std::vector McBopomofo::DictionaryServices::menuForPhrase( + const std::string& phrase) { + std::vector menu; + for (const auto& service : services_) { + std::string item = service->textForMenu(phrase); + menu.emplace_back(item); + } + return menu; +} + +void McBopomofo::DictionaryServices::load() { + services_.emplace_back(std::make_unique()); + + // Load json and add to services_ + std::string dictionaryServicesPath = fcitx::StandardPath::global().locate( + fcitx::StandardPath::Type::PkgData, kDataPath); + FILE* file = fopen(dictionaryServicesPath.c_str(), "r"); + if (!file) { + FCITX_MCBOPOMOFO_INFO() + << "No dictionary service file" << dictionaryServicesPath; + return; + } + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + fseek(file, 0, SEEK_SET); + std::unique_ptr json_data(new char[file_size + 1]); + fread(json_data.get(), 1, file_size, file); + fclose(file); + json_data[file_size] = '\0'; + struct json_object* json_obj = json_tokener_parse(json_data.get()); + if (json_obj == nullptr) { + return; + } + struct json_object* servicesArray; + if (json_object_object_get_ex(json_obj, "services", &servicesArray)) { + array_list* list = json_object_get_array(servicesArray); + for (size_t i = 0; i < array_list_length(list); i++) { + auto* element = (struct json_object*)array_list_get_idx(list, i); + struct json_object* name; + struct json_object* url_template; + if (json_object_object_get_ex(element, "name", &name) && + json_object_object_get_ex(element, "url_template", &url_template)) { + std::string name_str = std::string(json_object_get_string(name)); + std::string url_template_str = + std::string(json_object_get_string(url_template)); + auto service = std::make_unique( + name_str, url_template_str); + services_.push_back(std::move(service)); + } + } + } + json_object_put(json_obj); +} + +McBopomofo::DictionaryServices::~DictionaryServices() = default; diff --git a/src/DictionaryService.h b/src/DictionaryService.h new file mode 100644 index 0000000..77b53c0 --- /dev/null +++ b/src/DictionaryService.h @@ -0,0 +1,60 @@ + +#ifndef FCITX5_MCBOPOMOFO_DICTIONARYSERVICE_H +#define FCITX5_MCBOPOMOFO_DICTIONARYSERVICE_H + +#include +#include +#include + +#include "InputState.h" + +namespace McBopomofo { + +using StateCallback = + std::function)>; + +/** + * Represents a single dictionary service. + */ +class DictionaryService { + public: + virtual ~DictionaryService() {}; + + virtual std::string name() const = 0; + virtual void lookup(std::string phrase, InputState* state, size_t serviceIndex, + const StateCallback& stateCallback) = 0; + virtual std::string textForMenu(std::string selectedString) const = 0; +}; + +/** Provides dictionaries that helps user to look up phrases. */ +class DictionaryServices { + public: + DictionaryServices(); + ~DictionaryServices(); + /** Whether if there is a list of services. */ + bool hasServices(); + /** Load additional services. */ + void load(); + /** + * Look up a phrase using the index of the service in the list. + * @param phrase The phrase. + * @param serviceIndex Index of the service. + * @param state The current state. + * @param stateCallback The state callback. + */ + void lookup(std::string phrase, size_t serviceIndex, InputState* state, + const StateCallback& stateCallback); + /** + * Create a menu by passing the selected phrase. + * @param phrase The phrase. + * @return The menu. + */ + std::vector menuForPhrase(const std::string& phrase); + + protected: + std::vector> services_; +}; + +} // namespace McBopomofo + +#endif // FCITX5_MCBOPOMOFO_DICTIONARYSERVICE_H diff --git a/src/InputMacro.cpp b/src/InputMacro.cpp index 3253ae0..d5ed460 100644 --- a/src/InputMacro.cpp +++ b/src/InputMacro.cpp @@ -24,8 +24,8 @@ class InputMacroDate : public InputMacro { public: InputMacroDate(std::string macroName, std::string calendar, int offset, icu::DateFormat::EStyle style) - : name_(macroName), - calendarName_(calendar), + : name_(std::move(macroName)), + calendarName_(std::move(calendar)), dayOffset_(offset), dateStyle_(style) {} std::string name() const override { return name_; } @@ -43,10 +43,10 @@ class InputMacroDate : public InputMacro { class InputMacroYear : public InputMacro { public: InputMacroYear(std::string macroName, std::string calendar, int offset, icu::UnicodeString pattern) - : name_(macroName), - calendarName_(calendar), + : name_(std::move(macroName)), + calendarName_(std::move(calendar)), yearOffset_(offset), - pattern_(pattern) {} + pattern_(std::move(pattern)) {} std::string name() const override { return name_; } std::string replacement() const override { return formatWithPattern(calendarName_, yearOffset_, /*dateOffset*/ 0, pattern_) + "年"; @@ -63,10 +63,10 @@ class InputMacroYear : public InputMacro { class InputMacroDayOfTheWeek : public InputMacro { public: InputMacroDayOfTheWeek(std::string macroName, std::string calendar, int offset, icu::UnicodeString pattern) - : name_(macroName), - calendarName_(calendar), + : name_(std::move(macroName)), + calendarName_(std::move(calendar)), dayOffset_(offset), - pattern_(pattern) {} + pattern_(std::move(pattern)) {} std::string name() const override { return name_; } std::string replacement() const override { return formatWithPattern(calendarName_, /*yearOffset*/ 0, dayOffset_, pattern_); @@ -600,7 +600,7 @@ std::string formatWithPattern(std::string calendarName, int yearOffset, int date std::string formatDate(std::string calendarName, int dayOffset, icu::DateFormat::EStyle dateStyle) { - return formatWithStyle(calendarName, /*yearOffset*/ 0, dayOffset, dateStyle, /*timeStyle*/ icu::DateFormat::EStyle::kNone); + return formatWithStyle(std::move(calendarName), /*yearOffset*/ 0, dayOffset, dateStyle, /*timeStyle*/ icu::DateFormat::EStyle::kNone); } std::string formatTime(icu::DateFormat::EStyle timeStyle) { diff --git a/src/InputState.h b/src/InputState.h index 9de166d..9aa1130 100644 --- a/src/InputState.h +++ b/src/InputState.h @@ -69,6 +69,7 @@ struct NotEmpty : InputState { : composingBuffer(std::move(buf)), cursorIndex(index), tooltip(tooltipText) {} + ~NotEmpty() override = default; const std::string composingBuffer; @@ -101,6 +102,11 @@ struct ChoosingCandidate : NotEmpty { const std::string reading; const std::string value; }; + + std::unique_ptr copy() { + return std::make_unique( + composingBuffer, cursorIndex, candidates); + } }; inline bool operator==(const ChoosingCandidate::Candidate& a, @@ -136,6 +142,58 @@ struct Marking : NotEmpty { const std::string tail; const std::string reading; const bool acceptable; + + std::unique_ptr copy() { + return std::make_unique( + composingBuffer, cursorIndex, tooltip, markStartGridCursorIndex, head, + markedText, tail, reading, acceptable); + } +}; + +struct SelectingDictionary : NotEmpty { + SelectingDictionary(std::unique_ptr previousState, + std::string selectedPhrase, size_t selectedIndex, + std::vector menu) + : NotEmpty(previousState->composingBuffer, previousState->cursorIndex, + previousState->tooltip), + previousState(std::move(previousState)), + selectedPhrase(std::move(selectedPhrase)), + selectedCandidateIndex(selectedIndex), + menu(std::move(menu)) {} + + std::unique_ptr previousState; + std::string selectedPhrase; + size_t selectedCandidateIndex; + std::vector menu; + + std::unique_ptr copy() { + ChoosingCandidate* choosingCandidate = + dynamic_cast(previousState.get()); + Marking* marking = dynamic_cast(previousState.get()); + std::unique_ptr copy; + + if (choosingCandidate != nullptr) { + copy = choosingCandidate->copy(); + } else if (marking != nullptr) { + copy = marking->copy(); + } + + return std::make_unique( + std::move(copy), selectedPhrase, selectedCandidateIndex, menu); + } +}; + +struct ShowingCharInfo : NotEmpty { + ShowingCharInfo(std::unique_ptr previousState, + std::string selectedPhrase) + : NotEmpty(previousState->previousState->composingBuffer, + previousState->previousState->cursorIndex, + previousState->previousState->tooltip), + previousState(std::move(previousState)), + selectedPhrase(std::move(selectedPhrase)) {} + + std::unique_ptr previousState; + std::string selectedPhrase; }; } // namespace InputStates diff --git a/src/KeyHandler.cpp b/src/KeyHandler.cpp index 95ca85d..28ca0fd 100644 --- a/src/KeyHandler.cpp +++ b/src/KeyHandler.cpp @@ -96,7 +96,10 @@ KeyHandler::KeyHandler( userPhraseAdder_(std::move(userPhraseAdder)), localizedStrings_(std::move(localizedStrings)), userOverrideModel_(kUserOverrideModelCapacity, kObservedOverrideHalfLife), - reading_(Formosa::Mandarin::BopomofoKeyboardLayout::StandardLayout()) {} + reading_(Formosa::Mandarin::BopomofoKeyboardLayout::StandardLayout()) { + dictionaryServices_ = std::make_unique(); + dictionaryServices_->load(); +} bool KeyHandler::handle(Key key, McBopomofo::InputState* state, const StateCallback& stateCallback, @@ -317,6 +320,20 @@ bool KeyHandler::handle(Key key, McBopomofo::InputState* state, return true; } + // Question key + if (simpleAscii == '?') { + auto* marking = dynamic_cast(state); + if (marking != nullptr) { + // Enter the state to select a dictionary service. + std::string markedText = marking->markedText; + std::unique_ptr copy = marking->copy(); + auto selecting = + buildSelectingDictionaryState(std::move(copy), markedText, 0); + stateCallback(std::move(selecting)); + return true; + } + } + // Punctuation key: backtick or grave accent. if (simpleAscii == kPunctuationListKey && lm_->hasUnigrams(kPunctuationListUnigramKey)) { @@ -339,6 +356,7 @@ bool KeyHandler::handle(Key key, McBopomofo::InputState* state, if (key.ascii != 0) { std::string chrStr(1, key.ascii); std::string unigram; + if (key.ctrlPressed) { unigram = std::string(kCtrlPunctuationKeyPrefix) + chrStr; return handlePunctuation(unigram, stateCallback, errorCallback); @@ -388,6 +406,13 @@ bool KeyHandler::handle(Key key, McBopomofo::InputState* state, // No key is handled. Refresh and consume the key. if (maybeNotEmptyState != nullptr) { + // It is possible that FCITX just passes a single shift key event here. + // When it is in the marking state, we do not want to go back to the + // inputting state anyway. + auto* marking = dynamic_cast(state); + if (marking != nullptr) { + return true; + } errorCallback(); stateCallback(buildInputtingState()); return true; @@ -411,6 +436,13 @@ void KeyHandler::candidateSelected( stateCallback(buildInputtingState()); } +void KeyHandler::dictionaryServiceSelected(std::string phrase, size_t index, + InputState* currentState, + const StateCallback& stateCallback) { + dictionaryServices_->lookup(std::move(phrase), index, currentState, + stateCallback); +} + void KeyHandler::candidatePanelCancelled(const StateCallback& stateCallback) { if (inputMode_ == InputMode::PlainBopomofo) { reset(); @@ -754,7 +786,7 @@ KeyHandler::ComposedString KeyHandler::getComposedString(size_t builderCursor) { // not-empty) between them. // // We'll also need to compute the UTF-8 cursor index. The idea here is we - // use a "running" index that will eventually catch the cursor index in the + // use a "running" index_ that will eventually catch the cursor index_ in the // builder. The tricky part is that if the spanning length of the node that // the cursor is at does not agree with the actual codepoint count of the // node's value, we'll need to move the cursor at the end of the node to @@ -910,6 +942,20 @@ std::unique_ptr KeyHandler::buildMarkingState( marked, tail, readingValue, isValid); } +bool KeyHandler::hasDictionaryServices() { + return dictionaryServices_->hasServices(); +} + +std::unique_ptr +KeyHandler::buildSelectingDictionaryState( + std::unique_ptr nonEmptyState, + const std::string& selectedPhrase, size_t selectedIndex) { + std::vector menu = + dictionaryServices_->menuForPhrase(selectedPhrase); + return std::make_unique( + std::move(nonEmptyState), selectedPhrase, selectedIndex, menu); +} + size_t KeyHandler::actualCandidateCursorIndex() { size_t cursor = grid_.cursor(); diff --git a/src/KeyHandler.h b/src/KeyHandler.h index 3ae0719..682fb27 100644 --- a/src/KeyHandler.h +++ b/src/KeyHandler.h @@ -30,6 +30,7 @@ #include #include +#include "DictionaryService.h" #include "Engine/Mandarin/Mandarin.h" #include "Engine/McBopomofoLM.h" #include "Engine/UserOverrideModel.h" @@ -75,6 +76,10 @@ class KeyHandler { const InputStates::ChoosingCandidate::Candidate& candidate, const StateCallback& stateCallback); + void dictionaryServiceSelected(std::string phrase, size_t index, + InputState* currentState, + const StateCallback& stateCallback); + // Candidate panel canceled. Can assume the context is in a candidate state. void candidatePanelCancelled(const StateCallback& stateCallback); @@ -86,6 +91,17 @@ class KeyHandler { void reset(); +#pragma region Dictionary Services + + bool hasDictionaryServices(); + + std::unique_ptr + buildSelectingDictionaryState( + std::unique_ptr nonEmptyState, + const std::string& selectedPhrase, size_t selectedIndex); + +#pragma endregion Dictionary Services + #pragma region Settings McBopomofo::InputMode inputMode(); @@ -171,6 +187,7 @@ class KeyHandler { UserOverrideModel userOverrideModel_; Formosa::Mandarin::BopomofoReadingBuffer reading_; Formosa::Gramambular2::ReadingGrid::WalkResult latestWalk_; + std::shared_ptr dictionaryServices_; #pragma region Settings diff --git a/src/McBopomofo.cpp b/src/McBopomofo.cpp index 3173eb0..669f417 100644 --- a/src/McBopomofo.cpp +++ b/src/McBopomofo.cpp @@ -124,6 +124,39 @@ class McBopomofoCandidateWord : public fcitx::CandidateWord { std::function callback_; }; +class McBopomofoDictionaryServiceWord : public fcitx::CandidateWord { + public: + McBopomofoDictionaryServiceWord( + fcitx::Text displayText, size_t index, + InputStates::SelectingDictionary* currentState, + std::shared_ptr keyHandler, + KeyHandler::StateCallback callback) + : fcitx::CandidateWord(std::move(displayText)), + index_(index), + currentState_(currentState), + keyHandler_(std::move(keyHandler)), + stateCallback_(std::move(callback)) {} + + void select(fcitx::InputContext* /*unused*/) const override { + keyHandler_->dictionaryServiceSelected( + currentState_->selectedPhrase, index_, currentState_, stateCallback_); + } + + private: + size_t index_; + InputStates::SelectingDictionary* currentState_; + std::shared_ptr keyHandler_; + KeyHandler::StateCallback stateCallback_; + std::function callback_; +}; + +class McBopomofoTextOnlyCandidateWord : public fcitx::CandidateWord { + public: + McBopomofoTextOnlyCandidateWord(fcitx::Text displayText) + : fcitx::CandidateWord(std::move(displayText)) {} + void select(fcitx::InputContext* /*unused*/) const override {} +}; + class KeyHandlerLocalizedString : public KeyHandler::LocalizedStrings { public: std::string cursorIsBetweenSyllables( @@ -378,7 +411,10 @@ void McBopomofoEngine::keyEvent(const fcitx::InputMethodEntry& /*unused*/, return; } - if (dynamic_cast(state_.get()) != nullptr) { + if (dynamic_cast(state_.get()) != nullptr || + dynamic_cast(state_.get()) != + nullptr || + dynamic_cast(state_.get()) != nullptr) { // Absorb all keys when the candidate panel is on. keyEvent.filterAndAccept(); @@ -395,7 +431,6 @@ void McBopomofoEngine::keyEvent(const fcitx::InputMethodEntry& /*unused*/, enterNewState(context, std::make_unique()); context->updateUserInterface(fcitx::UserInterfaceComponent::InputPanel); context->updatePreedit(); - return; } @@ -408,7 +443,10 @@ void McBopomofoEngine::keyEvent(const fcitx::InputMethodEntry& /*unused*/, // TODO(unassigned): beep? }); if (dynamic_cast(state_.get()) != - nullptr) { + nullptr || + dynamic_cast(state_.get()) != + nullptr || + dynamic_cast(state_.get()) != nullptr) { context->updateUserInterface(fcitx::UserInterfaceComponent::InputPanel); context->updatePreedit(); } @@ -447,6 +485,48 @@ void McBopomofoEngine::handleCandidateKeyEvent( return; } + bool keyIsCancel = false; + + // When pressing "?" in the candidate list, tries to look up the candidate in + // dictionaries. + if (key.check(FcitxKey_question)) { + auto choosingCandidate = + dynamic_cast(state_.get()); + auto selectingDictionary = + dynamic_cast(state_.get()); + auto showingCharInfo = + dynamic_cast(state_.get()); + + if (choosingCandidate != nullptr) { + // Enter selecting dictionary service state. + if (keyHandler_->hasDictionaryServices()) { +#ifdef USE_LEGACY_FCITX5_API + int page = candidateList->currentPage(); + int pageSize = candidateList->size(); + int selectedIndex = page * pageSize + candidateList->cursorIndex(); + std::string phrase = + candidateList->candidate(selectedIndex)->text().toString(); +#else + int selectedIndex = candidateList->globalCursorIndex(); + std::string phrase = + candidateList->candidate(selectedIndex).text().toString(); +#endif + std::unique_ptr copy = + choosingCandidate->copy(); + auto state = keyHandler_->buildSelectingDictionaryState( + std::move(copy), phrase, selectedIndex); + enterNewState(context, std::move(state)); + return; + } + } else if (selectingDictionary != nullptr) { + // Leave selecting dictionary service state. + keyIsCancel = true; + } else if (showingCharInfo != nullptr) { + // Leave selecting dictionary service state. + keyIsCancel = true; + } + } + if (key.check(FcitxKey_Return)) { idx = candidateList->cursorIndex(); if (idx < candidateList->size()) { @@ -459,7 +539,42 @@ void McBopomofoEngine::handleCandidateKeyEvent( return; } - if (key.check(FcitxKey_Escape) || key.check(FcitxKey_BackSpace)) { + if (keyIsCancel || key.check(FcitxKey_Escape) || + key.check(FcitxKey_BackSpace)) { + auto* showingCharInfo = + dynamic_cast(state_.get()); + if (showingCharInfo != nullptr) { + auto previous = showingCharInfo->previousState.get(); + stateCallback(previous->copy()); + return; + } + + auto* selecting = + dynamic_cast(state_.get()); + if (selecting != nullptr) { + auto previous = selecting->previousState.get(); + auto* choosing = dynamic_cast(previous); + if (choosing != nullptr) { + stateCallback(choosing->copy()); + +#ifdef USE_LEGACY_FCITX5_API + auto maybeCandidateList = dynamic_cast( + context->inputPanel().candidateList()); +#else + auto* maybeCandidateList = dynamic_cast( + context->inputPanel().candidateList().get()); +#endif + size_t index = selecting->selectedCandidateIndex; + maybeCandidateList->setGlobalCursorIndex((int)index); + return; + } + auto* marking = dynamic_cast(previous); + if (marking != nullptr) { + stateCallback(marking->copy()); + } + return; + } + keyHandler_->candidatePanelCancelled( [this, context](std::unique_ptr next) { enterNewState(context, std::move(next)); @@ -578,6 +693,7 @@ void McBopomofoEngine::enterNewState(fcitx::InputContext* context, if (auto* empty = dynamic_cast(currentPtr)) { handleEmptyState(context, prevPtr, empty); + } else if (auto* emptyIgnoringPrevious = dynamic_cast( currentPtr)) { @@ -595,6 +711,12 @@ void McBopomofoEngine::enterNewState(fcitx::InputContext* context, } else if (auto* candidates = dynamic_cast(currentPtr)) { handleCandidatesState(context, prevPtr, candidates); + } else if (auto* selecting = + dynamic_cast(currentPtr)) { + handleCandidatesState(context, prevPtr, selecting); + } else if (auto* showingCharInfo = + dynamic_cast(currentPtr)) { + handleCandidatesState(context, prevPtr, showingCharInfo); } else if (auto* marking = dynamic_cast(currentPtr)) { handleMarkingState(context, prevPtr, marking); } @@ -638,9 +760,9 @@ void McBopomofoEngine::handleInputtingState(fcitx::InputContext* context, updatePreedit(context, current); } -void McBopomofoEngine::handleCandidatesState( - fcitx::InputContext* context, InputState* /*unused*/, - InputStates::ChoosingCandidate* current) { +void McBopomofoEngine::handleCandidatesState(fcitx::InputContext* context, + InputState* /*unused*/, + InputStates::NotEmpty* current) { std::unique_ptr candidateList = std::make_unique(); @@ -685,45 +807,93 @@ void McBopomofoEngine::handleCandidatesState( fcitx::CandidateLayoutHint layoutHint = getCandidateLayoutHint(); candidateList->setLayoutHint(layoutHint); - // Construct the candidate list with special care for candidates that have - // the same values. The display text of such a candidate will be in the form - // of "value (reading)" to help user disambiguate those candidates. - - std::unordered_map valueCountMap; - KeyHandler::StateCallback callback = [this, context](std::unique_ptr next) { enterNewState(context, std::move(next)); }; - for (const auto& c : current->candidates) { - ++valueCountMap[c.value]; - } + auto choosing = dynamic_cast(current); + auto selectingDictionary = + dynamic_cast(current); + auto showingCharInfo = dynamic_cast(current); + + if (choosing != nullptr) { + // Construct the candidate list with special care for candidates that have + // the same values. The display text of such a candidate will be in the form + // of "value (reading)" to help user disambiguate those candidates. + + std::unordered_map valueCountMap; + + for (const auto& c : choosing->candidates) { + ++valueCountMap[c.value]; + } - for (const auto& c : current->candidates) { - std::string displayText = c.value; + for (const auto& c : choosing->candidates) { + std::string displayText = c.value; - if (valueCountMap[displayText] > 1) { - displayText += " ("; - std::string reading = c.reading; - std::replace(reading.begin(), reading.end(), - KeyHandler::kJoinSeparator[0], ' '); - displayText += reading; - displayText += ")"; + if (valueCountMap[displayText] > 1) { + displayText += " ("; + std::string reading = c.reading; + std::replace(reading.begin(), reading.end(), + KeyHandler::kJoinSeparator[0], ' '); + displayText += reading; + displayText += ")"; + } + +#ifdef USE_LEGACY_FCITX5_API + fcitx::CandidateWord* candidate = new McBopomofoCandidateWord( + fcitx::Text(displayText), c, keyHandler_, callback); + // ownership of candidate is transferred to candidateList. + candidateList->append(candidate); +#else + std::unique_ptr candidate = + std::make_unique(fcitx::Text(displayText), c, + keyHandler_, callback); + candidateList->append(std::move(candidate)); +#endif + } + } else if (selectingDictionary != nullptr) { + size_t index = 0; + for (const auto& menuItem : selectingDictionary->menu) { + std::string displayText = menuItem; + +#ifdef USE_LEGACY_FCITX5_API + fcitx::CandidateWord* candidate = new McBopomofoDictionaryServiceWord( + fcitx::Text(displayText), index, selectingDictionary, keyHandler_, + callback); + candidateList->append(candidate); +#else + std::unique_ptr candidate = + std::make_unique( + fcitx::Text(displayText), index, selectingDictionary, keyHandler_, + callback); + candidateList->append(std::move(candidate)); +#endif + index++; } + } else if (showingCharInfo != nullptr) { + std::vector menu; + menu.emplace_back(fmt::format(_("UTF8 String Length: {0}"), + showingCharInfo->selectedPhrase.length())); + size_t count = CodePointCount(showingCharInfo->selectedPhrase); + menu.emplace_back(fmt::format(_("Code Point Count: {0}"), count)); + + for (const auto& menuItem : menu) { + std::string displayText = menuItem; #ifdef USE_LEGACY_FCITX5_API - fcitx::CandidateWord* candidate = new McBopomofoCandidateWord( - fcitx::Text(displayText), c, keyHandler_, callback); - // ownership of candidate is transferred to candidateList. - candidateList->append(candidate); + fcitx::CandidateWord* candidate = + new McBopomofoTextOnlyCandidateWord(fcitx::Text(displayText)); + candidateList->append(candidate); #else - std::unique_ptr candidate = - std::make_unique(fcitx::Text(displayText), c, - keyHandler_, callback); - candidateList->append(std::move(candidate)); + std::unique_ptr candidate = + std::make_unique( + fcitx::Text(displayText)); + candidateList->append(std::move(candidate)); #endif + } } + candidateList->toCursorMovable()->nextCandidate(); context->inputPanel().reset(); context->inputPanel().setCandidateList(std::move(candidateList)); @@ -743,6 +913,14 @@ void McBopomofoEngine::handleMarkingState(fcitx::InputContext* context, fcitx::CandidateLayoutHint McBopomofoEngine::getCandidateLayoutHint() const { fcitx::CandidateLayoutHint layoutHint = fcitx::CandidateLayoutHint::NotSet; + if (dynamic_cast(state_.get()) != + nullptr) { + return fcitx::CandidateLayoutHint::Vertical; + } + if (dynamic_cast(state_.get()) != nullptr) { + return fcitx::CandidateLayoutHint::Vertical; + } + auto choosingCandidate = dynamic_cast(state_.get()); if (choosingCandidate != nullptr) { diff --git a/src/McBopomofo.h b/src/McBopomofo.h index e1bfbec..61eec35 100644 --- a/src/McBopomofo.h +++ b/src/McBopomofo.h @@ -147,9 +147,7 @@ FCITX_CONFIGURATION( fcitx::Option addScriptHookEnabled{ this, "AddScriptHookEnabled", - _("Run the hook script after adding a phrase"), false}; - -); + _("Run the hook script after adding a phrase"), false};); class McBopomofoEngine : public fcitx::InputMethodEngine { public: @@ -192,7 +190,7 @@ class McBopomofoEngine : public fcitx::InputMethodEngine { void handleInputtingState(fcitx::InputContext* context, InputState* prev, InputStates::Inputting* current); void handleCandidatesState(fcitx::InputContext* context, InputState* prev, - InputStates::ChoosingCandidate* current); + InputStates::NotEmpty* current); void handleMarkingState(fcitx::InputContext* context, InputState* prev, InputStates::Marking* current);