├── .gitignore ├── .gitmodules ├── Lib ├── GetCaretPosEx │ ├── README.md │ ├── LICENSE.txt │ ├── GetCaretPosEx.patch │ └── GetCaretPosEx.ahk ├── RabbitCaret.ahk ├── RabbitConfig.ahk ├── RabbitMonitors.ahk ├── RabbitCommon.ahk ├── RabbitTrayMenu.ahk ├── RabbitKeyTable.ahk ├── RabbitUIStyle.ahk ├── RabbitThemesUI.ahk └── RabbitCandidateBox.ahk ├── assets ├── zhuyin-t.svg ├── pinyin-t.svg ├── pinyin-t-olivedrab.svg ├── pinyin-t-alt.svg └── zhuyin-t-alt.svg ├── README.md ├── .github └── workflows │ └── ci.yaml ├── schemas └── rabbit.yaml ├── Rabbit.ahk └── RabbitDeployer.ahk /.gitignore: -------------------------------------------------------------------------------- 1 | Rime 2 | Data 3 | dist 4 | test.ahk 5 | *.exe 6 | *.dll 7 | *.ico 8 | *.obj 9 | *.portable 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Lib/librime-ahk"] 2 | path = Lib/librime-ahk 3 | url = https://github.com/rimeinn/librime-ahk 4 | [submodule "plum"] 5 | path = plum 6 | url = https://github.com/rime/plum 7 | [submodule "Lib/Direct2D"] 8 | path = Lib/Direct2D 9 | url = https://github.com/rawbx/AHK-Direct2D 10 | -------------------------------------------------------------------------------- /Lib/GetCaretPosEx/README.md: -------------------------------------------------------------------------------- 1 | # GetCaretPosEx 2 | 3 | - 来源 [Tebayaki/AutoHotkeyScripts](https://github.com/Tebayaki/AutoHotkeyScripts) 4 | - 开源协议 [MIT](LICENSE.txt) 5 | - 版本 [Tebayaki/AutoHotkeyScripts@fcef513](https://github.com/Tebayaki/AutoHotkeyScripts/tree/fcef513b3a7e5f2ecc458d1f02b833e58cfbcd83/lib/GetCaretPosEx) 6 | -------------------------------------------------------------------------------- /assets/zhuyin-t.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/pinyin-t.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/pinyin-t-olivedrab.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/pinyin-t-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/zhuyin-t-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Lib/GetCaretPosEx/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tebayaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Lib/GetCaretPosEx/GetCaretPosEx.patch: -------------------------------------------------------------------------------- 1 | diff --git a/Lib/GetCaretPosEx/GetCaretPosEx.ahk b/Lib/GetCaretPosEx/GetCaretPosEx.ahk 2 | index ff9a7f7..ac6cc79 100644 3 | --- a/Lib/GetCaretPosEx/GetCaretPosEx.ahk 4 | +++ b/Lib/GetCaretPosEx/GetCaretPosEx.ahk 5 | @@ -14,10 +14,17 @@ GetCaretPosEx(&left?, &top?, &right?, &bottom?, useHook := false) { 6 | className := WinGetClass(hwnd) 7 | catch 8 | className := "" 9 | + 10 | + ; adobe typing mode is not a normal input mode, exclude it 11 | + if className == "PSViewC" or className == "DroverLord - Window Class" 12 | + return false 13 | + 14 | if className ~= "^(?:Windows|Microsoft)\.UI\..+" 15 | funcs := [getCaretPosFromUIA, getCaretPosFromHook, getCaretPosFromMSAA] 16 | else if className ~= "^HwndWrapper\[PowerShell_ISE\.exe;;[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\]" 17 | funcs := [getCaretPosFromHook, getCaretPosFromWpfCaret] 18 | + else if className ~= "^Chrome_WidgetWin_.+" 19 | + funcs := [getCaretPosFromUIA, getCaretPosFromHook] 20 | else 21 | funcs := [getCaretPosFromMSAA, getCaretPosFromUIA, getCaretPosFromHook] 22 | for fn in funcs { 23 | @@ -332,8 +339,12 @@ end: 24 | } 25 | 26 | static getWindowScale(hwnd) { 27 | - if winDpi := DllCall("GetDpiForWindow", "ptr", hwnd, "uint") 28 | - return A_ScreenDPI / winDpi 29 | + try { 30 | + if winDpi := DllCall("GetDpiForWindow", "ptr", hwnd, "uint") 31 | + return A_ScreenDPI / winDpi 32 | + } catch { 33 | + ; ignore error 34 | + } 35 | return 1 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Lib/RabbitCaret.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 - 2025 Xuesong Peng 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | /* 20 | * original code can be found in https://github.com/Descolada/AHK-v2-libraries 21 | * with MIT License 22 | */ 23 | 24 | #Include 25 | 26 | /** 27 | * Gets the position of the caret with UIA, Acc or CaretGetPos. 28 | * Credit: plankoe (https://www.reddit.com/r/AutoHotkey/comments/ysuawq/get_the_caret_location_in_any_program/) 29 | * @param X Value is set to the screen X-coordinate of the caret 30 | * @param Y Value is set to the screen Y-coordinate of the caret 31 | * @param W Value is set to the width of the caret 32 | * @param H Value is set to the height of the caret 33 | */ 34 | GetCaretPos(&caret_x?, &caret_y?, &caret_w?, &caret_h?) { 35 | caret_x := 0 36 | caret_y := 0 37 | caret_w := 0 38 | caret_h := 0 39 | 40 | if GetCaretPosEx(&left, &top, &right, &bottom, true) { 41 | if !IsSet(left) || !IsSet(top) || !IsSet(right) || !IsSet(bottom) 42 | return GetBuiltInCaretPos(&caret_x, &caret_y, &caret_w, &caret_h) 43 | 44 | local max_int := 2147483647 45 | local max_uint := 4294967295 46 | caret_x := left 47 | caret_y := top 48 | caret_w := right - left 49 | caret_h := bottom - top 50 | if caret_x > max_int 51 | caret_x := caret_x - max_uint - 1 52 | if caret_y > max_int 53 | caret_y := caret_y - max_uint - 1 54 | 55 | return true 56 | } 57 | 58 | return GetBuiltInCaretPos(&caret_x, &caret_y, &caret_w, &caret_h) 59 | } 60 | 61 | GetBuiltInCaretPos(&x, &y, &w, &h) { 62 | local saved_caret := A_CoordModeCaret 63 | CoordMode("Caret", "Screen") 64 | local found := CaretGetPos(&x, &y) 65 | CoordMode("Caret", saved_caret) 66 | if found { 67 | w := 4 68 | h := 20 69 | } else { 70 | x := 0 71 | y := 0 72 | } 73 | return found 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🐇️玉兔毫 2 | 3 | 由 [AutoHotkey](https://www.autohotkey.com/) 实现的 [Rime 输入法引擎](https://github.com/rime/librime)前端 4 | 5 | [![Download](https://img.shields.io/github/v/release/rimeinn/rabbit)](https://github.com/rimeinn/rabbit/releases/latest) 6 | [![Build Status](https://github.com/rimeinn/rabbit/actions/workflows/ci.yaml/badge.svg)](https://github.com/rimeinn/rabbit/actions/workflows/ci.yaml) 7 | [![Telegram Group Chat](https://telegram-badge.vercel.app/api/telegram-badge?channelId=@rime_rabbit)](https://t.me/rime_rabbit) 8 | [![License](https://img.shields.io/github/license/rimeinn/rabbit)](LICENSE) 9 | [![GitHub Repo stars](https://img.shields.io/github/stars/rimeinn/rabbit?style=flat)](https://github.com/rimeinn/rabbit/stargazers) 10 | 11 | ## 下载体验 12 | 13 | > [!NOTE] 14 | > 发现程序漏洞请在 [Issues](https://github.com/rimeinn/rabbit/issues/new/choose) 反馈。使用问题可以在 [Discussions](https://github.com/rimeinn/rabbit/discussions) 讨论,或者加入 [Telegram 群聊](https://t.me/rime_rabbit)。 15 | 16 | ### 通过发布页面下载 17 | 18 | 正式发行版会在 [Release](https://github.com/rimeinn/rabbit/releases) 页面的 Assets 中,下载最新的 `rabbit-v<版本号>.zip`,解压到一个新建文件夹,运行 `Rabbit.exe` 即可。 19 | 20 | 每夜构建版可在 [`latest`](https://github.com/rimeinn/rabbit/releases/tag/latest) 页面下载。 21 | 22 | ### 通过 [scoop](https://scoop.sh/) 安装 23 | 24 | ```PowerShell 25 | scoop bucket add siku https://github.com/amorphobia/siku 26 | # 正式发行版 27 | scoop install siku/rabbit 28 | # 每夜构建版 29 | scoop install siku/rabbit-nightly 30 | ``` 31 | 32 | ## 脚本编译 33 | 34 | 本仓库提供*源码形式的玉兔毫脚本*以及*仅修改主图标的 AutoHotkey 可执行文件*,用户可根据需要自行编译为可执行文件以及压缩。编译方式可参照 AutoHotkey 的[官方文档](https://www.autohotkey.com/docs/v2/Scripts.htm#ahk2exe)。 35 | 36 | 编译并使用 `upx` 压缩后,64 位的可执行文件大小可减少为 `Rabbit.exe` - 约 570+ KB, `RabbitDeployer.exe` - 约 560+ KB。 37 | 38 | ## 目录结构 39 | 40 |
41 | 点击展开 42 | 43 | > 以下描述的*可删除*、*编译后可删除*指的是删除后不影响使用,若要再次分发脚本或编译后的可执行文件,需遵守 [GPL-3.0 开源许可](LICENSE)。 44 | 45 | ``` 46 | rabbit/ 47 | ├─ Data/ 预设方案以及必要配置,内容删除后可能无法正常使用,若用户目录包含所有必要文件,可删除 48 | ├─ Lib/ 玉兔毫运行依赖脚本库,编译后可删除 49 | | ├─ librime-ahk Rime 引擎的 AutoHotkey 绑定,编译后可删除 50 | | | ├─ rime.dll Rime 引擎的动态库,若本机已安装小狼毫,可删除;若没有安装小狼毫,需要 a. 保留在此,或 b. 放到主目录,或 c. 放到环境变量 "LIBRIME_LIB_DIR" 指定的目录 51 | | | ├─ ... librime-ahk 库的其他脚本,编译后可删除 52 | | ├─ ... 其他依赖,编译后可删除 53 | ├─ plum/ 若使用东风破,将被安装到此路径 54 | ├─ Rime/ Rime 用户文件夹,运行后会自动生成;可修改注册表 "HKEY_CURRENT_USER\Software\Rime\Rabbit" 中的 "RimeUserDir" 来指定不同的用户文件夹 55 | ├─ LICENSE 开源许可,可删除 56 | ├─ Rabbit.ahk 玉兔毫主程序脚本 57 | ├─ Rabbit.exe AutoHotkey 可执行文件,若本机已安装 AutoHotkey 或已编译,可删除 58 | ├─ RabbitDeployer.ahk 玉兔毫部署应用脚本 59 | ├─ README.md 本文件,可删除 60 | ├─ rime-install.bat 东风破批处理脚本,删除后无法从设定中调用东风破 61 | ``` 62 | 63 |
64 | 65 | ## 使用的开源项目 66 | 67 | - [librime](https://github.com/rime/librime) 68 | - [librime-ahk](https://github.com/rimeinn/librime-ahk) 69 | - [AHK-Direct2D](https://github.com/rawbx/AHK-Direct2D) 70 | - [OpenCC](https://github.com/BYVoid/OpenCC) 71 | - [GetCaretPos](https://github.com/Descolada/AHK-v2-libraries) 72 | - [GetCaretPosEx](https://github.com/Tebayaki/AutoHotkeyScripts/tree/main/lib/GetCaretPosEx) 73 | - [东风破](https://github.com/rime/plum) 74 | - [小狼毫](https://github.com/rime/weasel) 75 | 76 | 以及一些代码片段,在注释中注明了来源链接 77 | 78 | ## 已知问题 79 | 80 | - 某些情况无法获得输入光标的坐标 81 | - 桌面版 QQ 的密码输入框无法使用:[QQ密码输入框(防键盘钩子)原理分析](https://blog.csdn.net/muyedongfeng/article/details/49308993), 82 | ([页面存档备份](https://web.archive.org/web/20240907052640/https://blog.csdn.net/muyedongfeng/article/details/49308993),存于互联网档案馆),可右键点击任务栏图标选择禁用/启用玉兔毫,或是在 `rabbit.custom.yaml` 里设置 `suspend_hotkey` 指定快捷键来禁用/启用玉兔毫 83 | - 在 Windows 7 中打开玉兔毫时可能会造成系统一段时间无响应,需等待初始化完成,原因未知 84 | -------------------------------------------------------------------------------- /Lib/RabbitConfig.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Xuesong Peng 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | #Include 20 | #Include 21 | 22 | class RabbitConfig { 23 | static suspend_hotkey := "" 24 | static show_tips := true 25 | static show_tips_time := 1200 26 | static global_ascii := false 27 | static preset_process_ascii := Map() 28 | static schema_icon := Map() 29 | static fix_candidate_box := false 30 | static use_legacy_candidate_box := false 31 | static send_by_clipboard_length := 8 32 | 33 | static load() { 34 | global rime, IS_DARK_MODE 35 | if !rime || !config := rime.config_open("rabbit") 36 | return 37 | 38 | RabbitConfig.suspend_hotkey := rime.config_get_string(config, "suspend_hotkey") 39 | if rime.config_test_get_bool(config, "show_tips", &result) 40 | RabbitConfig.show_tips := !!result 41 | if rime.config_test_get_int(config, "show_tips_time", &result) { 42 | RabbitConfig.show_tips_time := Abs(result) 43 | if result == 0 44 | RabbitConfig.show_tips := false 45 | } 46 | 47 | if rime.config_test_get_int(config, "send_by_clipboard_length", &result) 48 | ; 0: always send by clipboard 49 | ; >0: send by clipboard if length >= value 50 | ; <0: never send by clipboard (65535 is large enough for candidates) 51 | RabbitConfig.send_by_clipboard_length := result >= 0 ? result : 65535 52 | 53 | if rime.config_test_get_bool(config, "global_ascii", &result) 54 | RabbitConfig.global_ascii := !!result 55 | 56 | if iter := rime.config_begin_map(config, "app_options") { 57 | while rime.config_next(iter) { 58 | proc_name := StrLower(iter.key) 59 | if rime.config_test_get_bool(config, "app_options/" . proc_name . "/ascii_mode", &result) { 60 | RabbitConfig.preset_process_ascii[proc_name] := !!result 61 | RabbitGlobals.process_ascii[proc_name] := !!result 62 | } 63 | } 64 | rime.config_end(iter) 65 | } 66 | 67 | if rime.config_test_get_bool(config, "fix_candidate_box", &result) 68 | RabbitConfig.fix_candidate_box := !!result 69 | if rime.config_test_get_bool(config, "use_legacy_candidate_box", &result) 70 | RabbitConfig.use_legacy_candidate_box := !!result 71 | 72 | UIStyle.Update(config, true) 73 | if IS_DARK_MODE := RabbitIsUserDarkMode() { 74 | if color_name := rime.config_get_string(config, "style/color_scheme_dark") 75 | UIStyle.use_dark := UIStyle.UpdateColor(config, color_name) 76 | DarkMode.set(IS_DARK_MODE) 77 | } 78 | 79 | rime.config_close(config) 80 | 81 | if !schema_list := rime.get_schema_list() 82 | return 83 | 84 | Loop schema_list.size { 85 | local item := schema_list.list[A_Index] 86 | if !schema := rime.schema_open(item.schema_id) 87 | continue 88 | 89 | if rime.config_test_get_string(schema, "schema/icon", &icon) { 90 | icon_path := RabbitUserDataPath() . "\" . LTrim(icon, "\") 91 | if !FileExist(icon_path) 92 | icon_path := RabbitSharedDataPath() . "\" . LTrim(icon, "\") 93 | RabbitConfig.schema_icon[item.schema_id] := FileExist(icon_path) ? icon_path : "" 94 | } else 95 | RabbitConfig.schema_icon[item.schema_id] := "" 96 | 97 | rime.config_close(schema) 98 | } 99 | 100 | rime.free_schema_list(schema_list) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Lib/RabbitMonitors.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, 2024 Xuesong Peng 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | global A_IntSize := 4 20 | global A_WCharSize := 2 21 | 22 | global CCHDEVICENAME := 32 23 | global MONITOR_DEFAULTTONULL := 0 24 | global MONITOR_DEFAULTTOPRIMARY := 1 25 | global MONITOR_DEFAULTTONEAREST := 2 26 | 27 | class Point extends Buffer { 28 | __New(x := 0, y := 0) { 29 | super.__New(Point.struct_size, 0) 30 | this.x := x 31 | this.y := y 32 | } 33 | 34 | static x_offset := 0 35 | static y_offset := Point.x_offset + A_IntSize 36 | static struct_size := Point.y_offset + A_IntSize 37 | 38 | x { 39 | get => NumGet(this, Point.x_offset, "Int") 40 | set => NumPut("Int", Value, this, Point.x_offset) 41 | } 42 | y { 43 | get => NumGet(this, Point.y_offset, "Int") 44 | set => NumPut("Int", Value, this, Point.y_offset) 45 | } 46 | } ; Point 47 | 48 | class Rect extends Buffer { 49 | __New(left := 0, top := 0, right := 0, bottom := 0) { 50 | super.__New(Rect.struct_size, 0) 51 | this.left := left 52 | this.top := top 53 | this.right := right 54 | this.bottom := bottom 55 | } 56 | 57 | static left_offset := 0 58 | static top_offset := Rect.left_offset + A_IntSize 59 | static right_offset := Rect.top_offset + A_IntSize 60 | static bottom_offset := Rect.right_offset + A_IntSize 61 | static struct_size := Rect.bottom_offset + A_IntSize 62 | 63 | left { 64 | get => NumGet(this, Rect.left_offset, "Int") 65 | set => NumPut("Int", Value, this, Rect.left_offset) 66 | } 67 | top { 68 | get => NumGet(this, Rect.top_offset, "Int") 69 | set => NumPut("Int", Value, this, Rect.top_offset) 70 | } 71 | right { 72 | get => NumGet(this, Rect.right_offset, "Int") 73 | set => NumPut("Int", Value, this, Rect.right_offset) 74 | } 75 | bottom { 76 | get => NumGet(this, Rect.bottom_offset, "Int") 77 | set => NumPut("Int", Value, this, Rect.bottom_offset) 78 | } 79 | 80 | width() { 81 | return this.right - this.left 82 | } 83 | height() { 84 | return this.bottom - this.top 85 | } 86 | } ; Rect 87 | 88 | class MonitorInfo extends Buffer { 89 | __New(bytes := MonitorInfo.struct_size, fill := 0) { 90 | super.__New(bytes, fill) 91 | NumPut("Int", MonitorInfo.struct_size, this, MonitorInfo.size_offset) 92 | } 93 | 94 | static size_offset := 0 95 | static monitor_offset := MonitorInfo.size_offset + A_IntSize 96 | static work_offset := MonitorInfo.monitor_offset + Rect.struct_size 97 | static flags_offset := MonitorInfo.work_offset + Rect.struct_size 98 | static struct_size := MonitorInfo.flags_offset + A_IntSize 99 | 100 | size { 101 | get => NumGet(this, MonitorInfo.size_offset, "Int") 102 | } 103 | monitor { 104 | get => Rect( 105 | NumGet(this, MonitorInfo.monitor_offset, "Int"), 106 | NumGet(this, MonitorInfo.monitor_offset + A_IntSize, "Int"), 107 | NumGet(this, MonitorInfo.monitor_offset + A_IntSize * 2, "Int"), 108 | NumGet(this, MonitorInfo.monitor_offset + A_IntSize * 3, "Int") 109 | ) 110 | } 111 | work { 112 | get => Rect( 113 | NumGet(this, MonitorInfo.work_offset, "Int"), 114 | NumGet(this, MonitorInfo.work_offset + A_IntSize, "Int"), 115 | NumGet(this, MonitorInfo.work_offset + A_IntSize * 2, "Int"), 116 | NumGet(this, MonitorInfo.work_offset + A_IntSize * 3, "Int") 117 | ) 118 | } 119 | flags { 120 | get => NumGet(this, MonitorInfo.flags_offset, "Int") 121 | } 122 | } ; MonitorInfo 123 | 124 | class MonitorInfoEx extends MonitorInfo { 125 | __New() { 126 | super.__New(MonitorInfoEx.struct_size, 0) 127 | NumPut("Int", MonitorInfoEx.struct_size, this, MonitorInfoEx.size_offset) 128 | } 129 | 130 | static device_offset := MonitorInfoEx.flags_offset + A_IntSize 131 | static struct_size := MonitorInfoEx.device_offset + A_WCharSize * CCHDEVICENAME 132 | 133 | device { 134 | get => StrGet(this.Ptr + MonitorInfoEx.device_offset, CCHDEVICENAME) 135 | } 136 | } ; MonitorInfoEx 137 | 138 | class MonitorManage extends Class { 139 | static monitors := Map() 140 | 141 | static MonitorEnumProc(hMon, hDC, rect, data) { 142 | MonitorManage.monitors[hMon] := MonitorManage.GetMonitorInfo(hMon) 143 | return true 144 | } 145 | 146 | static EnumDisplayMonitors() { 147 | MonitorManage.monitors := Map() 148 | return DllCall("EnumDisplayMonitors", "Ptr", 0, "Ptr", 0, "Ptr", CallbackCreate(ObjBindMethod(MonitorManage, "MonitorEnumProc"), , 4), "Ptr", 0) 149 | } 150 | 151 | static MonitorFromWindow(hWnd, flags := MONITOR_DEFAULTTONULL) { 152 | return DllCall("MonitorFromWindow", "Ptr", hWnd, "UInt", flags) 153 | } 154 | 155 | static MonitorFromPoint(point, flags := MONITOR_DEFAULTTONULL) { 156 | return DllCall("MonitorFromPoint", "Int64", (point.x & 0xFFFFFFFF) | (point.y << 32), "UInt", flags) 157 | } 158 | 159 | static MonitorFromRect(rect, flags := MONITOR_DEFAULTTONULL) { 160 | return DllCall("MonitorFromRect", "Ptr", rect, "UInt", flags) 161 | } 162 | 163 | static GetMonitorInfo(hMon) { 164 | info := MonitorInfoEx() 165 | res := DllCall("GetMonitorInfo", "Ptr", hMon, "Ptr", info) 166 | return res ? info : 0 167 | } 168 | } ; MonitorManage 169 | -------------------------------------------------------------------------------- /Lib/RabbitCommon.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 - 2025 Xuesong Peng 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | global RABBIT_VERSION := "dev" 20 | ;@Ahk2Exe-SetCompanyName rimeinn 21 | ;@Ahk2Exe-SetCopyright Copyright (c) 2023 - 2025 Xuesong Peng 22 | ;@Ahk2Exe-SetDescription 由 AutoHotkey 实现的 Rime 输入法 23 | ;@Ahk2Exe-Let U_version = %A_PriorLine~U)^(.+"){1}(.+)".*$~$2% 24 | ;@Ahk2Exe-SetVersion %U_version% 25 | ;@Ahk2Exe-SetLanguage 0x0804 26 | ;@Ahk2Exe-SetMainIcon Lib\rabbit.ico 27 | ;@Ahk2Exe-AddResource Lib\rabbit-ascii.ico, 160 28 | ;@Ahk2Exe-AddResource Lib\rabbit-alt.ico, 206 29 | 30 | #Include 31 | #Include 32 | 33 | global AHK_NOTIFYICON := 0x404 34 | global WM_LBUTTONDOWN := 0x201 35 | global WM_LBUTTONUP := 0x202 36 | global WM_SETTINGCHANGE := 0x001A 37 | global WM_DWMCOLORIZATIONCOLORCHANGED := 0x0320 38 | 39 | global rime := RimeApi(A_ScriptDir . "\Lib\librime-ahk\rime.dll") 40 | global RABBIT_IME_NAME := "玉兔毫" 41 | global RABBIT_CODE_NAME := "Rabbit" 42 | global RABBIT_NO_MAINTENANCE := "0" 43 | global RABBIT_PARTIAL_MAINTENANCE := "1" 44 | global RABBIT_FULL_MAINTENANCE := "2" 45 | 46 | global IN_MAINTENANCE := false 47 | global STATUS_TOOLTIP := 2 48 | global box := 0 49 | global rabbit_traits 50 | global IS_DARK_MODE := false 51 | global ASCII_MODE_FALSE_LABEL := "中文" 52 | global ASCII_MODE_TRUE_LABEL := "西文" 53 | global ASCII_MODE_FALSE_LABEL_ABBR := "中" 54 | global ASCII_MODE_TRUE_LABEL_ABBR := "西" 55 | global FULL_SHAPE_FALSE_LABEL := "半角" 56 | global FULL_SHAPE_TRUE_LABEL := "全角" 57 | global FULL_SHAPE_FALSE_LABEL_ABBR := "半" 58 | global FULL_SHAPE_TRUE_LABEL_ABBR := "全" 59 | global ASCII_PUNCT_FALSE_LABEL := "。," 60 | global ASCII_PUNCT_TRUE_LABEL := ". ," 61 | global ASCII_PUNCT_FALSE_LABEL_ABBR := "。" 62 | global ASCII_PUNCT_TRUE_LABEL_ABBR := "." 63 | 64 | global ERROR_ALREADY_EXISTS := 183 ; https://learn.microsoft.com/windows/win32/debug/system-error-codes--0-499- 65 | 66 | class RabbitGlobals { 67 | static process_ascii := Map() 68 | static on_tray_icon_click := false 69 | static active_win := "" 70 | static current_schema_icon := "" 71 | static keyboard_layout := 0x0409 72 | } 73 | 74 | class RabbitMutex { 75 | handle := 0 76 | lasterr := 0 77 | Create() { 78 | this.lasterr := 0 79 | this.handle := DllCall("CreateMutex", "Ptr", 0, "Int", true, "Str", "RabbitDeployerMutex") 80 | if A_LastError == ERROR_ALREADY_EXISTS { 81 | this.lasterr := ERROR_ALREADY_EXISTS 82 | } 83 | return this.handle 84 | } 85 | Close() { 86 | if this.handle { 87 | DllCall("CloseHandle", "Ptr", this.handle) 88 | this.handle := 0 89 | } 90 | } 91 | } 92 | 93 | CreateTraits() { 94 | traits := RimeTraits() 95 | traits.distribution_name := RABBIT_IME_NAME 96 | traits.distribution_code_name := RABBIT_CODE_NAME 97 | traits.distribution_version := RABBIT_VERSION 98 | traits.app_name := "rime.rabbit" 99 | traits.shared_data_dir := RabbitSharedDataPath() 100 | traits.user_data_dir := RabbitUserDataPath() 101 | traits.prebuilt_data_dir := traits.shared_data_dir 102 | traits.log_dir := RabbitLogPath() 103 | 104 | return traits 105 | } 106 | 107 | RabbitUserDataPath() { 108 | if FileExist(A_ScriptDir . "\.portable") { 109 | RabbitDebug("run in portable mode.", Format("RabbitCommon.ahk:{}", A_LineNumber), 1) 110 | return A_ScriptDir . "\Rime" 111 | } 112 | try { 113 | local dir := RegRead("HKEY_CURRENT_USER\Software\Rime\Rabbit", "RimeUserDir") 114 | } 115 | if IsSet(dir) && dir && Type(dir) = "String" { 116 | size := DllCall("ExpandEnvironmentStrings", "Str", dir, "Ptr", 0, "UInt", 0) 117 | path := Buffer(size * 2, 0) 118 | DllCall("ExpandEnvironmentStrings", "Str", dir, "Ptr", path, "UInt", path.Size) 119 | return StrGet(path) 120 | } 121 | return A_ScriptDir . "\Rime" 122 | } 123 | 124 | RabbitSharedDataPath() { 125 | return A_ScriptDir . "\Data" 126 | } 127 | 128 | RabbitLogPath() { 129 | path := A_Temp . "\rime.rabbit" 130 | if !DirExist(path) 131 | DirCreate(path) 132 | return path 133 | } 134 | 135 | OnRimeMessage(context_object, session_id, message_type, message_value) { 136 | msg_type := StrGet(message_type, "UTF-8") 137 | msg_value := StrGet(message_value, "UTF-8") 138 | if msg_type = "deploy" { 139 | if msg_value = "start" { 140 | TrayTip() 141 | TrayTip("维护中", RABBIT_IME_NAME) 142 | } else if msg_value = "success" { 143 | TrayTip() 144 | TrayTip("维护完成", RABBIT_IME_NAME) 145 | SetTimer(TrayTip, -2000) 146 | } else { 147 | TrayTip(msg_type . ": " . msg_value . " (" . session_id . ")", RABBIT_IME_NAME) 148 | } 149 | } else { 150 | ; TrayTip(msg_type . ": " . msg_value . " (" . session_id . ")", RABBIT_IME_NAME) 151 | } 152 | } 153 | 154 | CleanOldLogs() { 155 | app_name := "rime.rabbit" 156 | dir := RabbitLogPath() 157 | if !DirExist(dir) 158 | return 159 | 160 | files := [] 161 | try { 162 | loop files dir, "R" { 163 | if InStr(A_LoopFileAttrib, "N") && !InStr(A_LoopFileAttrib, "L") 164 | && SubStr(A_LoopFileName, 1, StrLen(app_name)) == app_name 165 | && SubStr(A_LoopFileName, -4) == ".log" 166 | && !InStr(A_LoopFileName, A_YYYY A_MM A_DD) { 167 | files.Push(A_LoopFileFullPath) 168 | } 169 | } 170 | } 171 | 172 | for file in files { 173 | try { 174 | FileDelete(file) 175 | } 176 | } 177 | } 178 | 179 | CleanMisPlacedConfigs() { 180 | shared := RabbitSharedDataPath() 181 | user := RabbitUserDataPath() 182 | 183 | if shared == user 184 | return 185 | 186 | if FileExist(user . "\default.yaml") { 187 | RabbitWarn(Format("renaming unnecessary file {}\default.yaml", user), Format("RabbitCommon.ahk:{}", A_LineNumber)) 188 | FileMove(user . "\default.yaml", user . "\default.yaml.old", 1) 189 | } 190 | if FileExist(user . "\rabbit.yaml") { 191 | RabbitWarn(Format("renaming unnecessary file {}\rabbit.yaml", user), Format("RabbitCommon.ahk:{}", A_LineNumber)) 192 | FileMove(user . "\rabbit.yaml", user . "\rabbit.yaml.old", 1) 193 | } 194 | } 195 | 196 | RabbitLog(text) { 197 | try { 198 | FileAppend(text, "*", "UTF-8") 199 | } 200 | } 201 | RabbitLogLimit(text, label, limit := 1) { 202 | static labels := Map() 203 | if !labels.Has(label) 204 | labels[label] := 0 205 | if limit < 0 || labels[label] < limit { 206 | RabbitLog(text) 207 | labels[label] := labels[label] + 1 208 | } 209 | } 210 | RabbitError(text, location, limit := -1) { 211 | msg := Format("E{} {:5} {}] {}`r`n", FormatTime(, "yyyyMMdd HH:mm:ss "), ProcessExist(), location, text) 212 | RabbitLogLimit(msg, location, limit) 213 | } 214 | RabbitWarn(text, location, limit := -1) { 215 | msg := Format("W{} {:5} {}] {}`r`n", FormatTime(, "yyyyMMdd HH:mm:ss "), ProcessExist(), location, text) 216 | RabbitLogLimit(msg, location, limit) 217 | } 218 | RabbitInfo(text, location, limit := -1) { 219 | msg := Format("I{} {:5} {}] {}`r`n", FormatTime(, "yyyyMMdd HH:mm:ss "), ProcessExist(), location, text) 220 | RabbitLogLimit(msg, location, limit) 221 | } 222 | RabbitDebug(text, location, limit := -1) { 223 | global RABBIT_VERSION 224 | if !SubStr(RABBIT_VERSION, 1, 3) = "dev" 225 | return 226 | msg := Format("D{} {:5} {}] {}`r`n", FormatTime(, "yyyyMMdd HH:mm:ss "), ProcessExist(), location, text) 227 | RabbitLogLimit(msg, location, limit) 228 | } 229 | -------------------------------------------------------------------------------- /Lib/RabbitTrayMenu.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 - 2025 Xuesong Peng 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | #Include 20 | #Include 21 | 22 | A_IconTip := "玉兔毫(维护中)" 23 | 24 | global TRAY_SCHEMA_NAME := "" 25 | global TRAY_ASCII_MODE := 0 26 | global TRAY_FULL_SHAPE := 0 27 | global TRAY_ASCII_PUNCT := 0 28 | 29 | UpdateTrayIcon() 30 | 31 | SetupTrayMenu() { 32 | static rabbit_script := Format("`"{}\Rabbit.ahk`"", A_ScriptDir) 33 | static rabbit_ico := Format("{}\Lib\rabbit.ico", A_ScriptDir) 34 | A_TrayMenu.Delete() 35 | if !IN_MAINTENANCE { 36 | A_TrayMenu.Add("输入法设定", (*) => RunDeployer("configure", RabbitGlobals.keyboard_layout)) 37 | A_TrayMenu.Add("用户词典管理", (*) => RunDeployer("dict", RabbitGlobals.keyboard_layout)) 38 | A_TrayMenu.Add("用户资料同步", (*) => RunDeployer("sync", RabbitGlobals.keyboard_layout)) 39 | 40 | A_TrayMenu.Add() 41 | 42 | A_TrayMenu.Add("用户文件夹", (*) => Run(RabbitUserDataPath())) 43 | A_TrayMenu.Add(A_IsCompiled ? "程序文件夹" : "脚本文件夹", (*) => Run(A_ScriptDir)) 44 | A_TrayMenu.Add("日志文件夹", (*) => Run(RabbitLogPath())) 45 | 46 | A_TrayMenu.Add() 47 | 48 | if FileExist(A_Startup . "\Rabbit.lnk") { 49 | A_TrayMenu.Add("从开机启动删除", (*) => (FileDelete(A_Startup . "\Rabbit.lnk"), SetupTrayMenu())) 50 | } else { 51 | A_TrayMenu.Add("添加到开机启动", (*) => (FileCreateShortcut(A_AhkPath, A_Startup . "\Rabbit.lnk", A_ScriptDir, rabbit_script, "玉兔毫输入法", rabbit_ico), SetupTrayMenu())) 52 | } 53 | A_TrayMenu.Add("添加到桌面快捷方式", (*) => FileCreateShortcut(A_AhkPath, A_Desktop . "\Rabbit.lnk", A_ScriptDir, rabbit_script, "玉兔毫输入法", rabbit_ico)) 54 | 55 | A_TrayMenu.Add() 56 | 57 | A_TrayMenu.Add("仓库主页", (*) => Run("https://github.com/rimeinn/rabbit")) 58 | A_TrayMenu.Add("参加讨论", (*) => Run("https://github.com/rimeinn/rabbit/discussions")) 59 | A_TrayMenu.Add("关于", (*) => MsgBox(Format("由 AutoHotkey 实现的 Rime 输入法引擎前端`r`n版本:{}{}", RABBIT_VERSION, A_IsCompiled ? "(已编译)" : ""), "玉兔毫输入法")) 60 | 61 | A_TrayMenu.Add() 62 | 63 | A_TrayMenu.Add("检查新版本", (*) => CheckNewVersion()) 64 | A_TrayMenu.Add("重新部署", (*) => RunDeployer("deploy", RabbitGlobals.keyboard_layout)) 65 | 66 | A_TrayMenu.Add() 67 | A_TrayMenu.Add(A_IsSuspended ? "启用玉兔毫" : "禁用玉兔毫", (*) => ToggleSuspend()) 68 | } 69 | A_TrayMenu.Add("退出玉兔毫", (*) => ExitApp()) 70 | } 71 | 72 | RunDeployer(cmd, argv*) { 73 | args := "" 74 | for arg in argv 75 | args .= " " . arg 76 | args := LTrim(args, " ") 77 | ; MsgBox(cmd . " " . args) 78 | if A_IsCompiled 79 | Run(Format("`"{}\RabbitDeployer.exe`" {} {}", A_ScriptDir, cmd, args)) 80 | else 81 | Run(Format("{} `"{}\RabbitDeployer.ahk`" {} {}", A_AhkPath, A_ScriptDir, cmd, args)) 82 | ExitApp(1) 83 | } 84 | 85 | ToggleSuspend() { 86 | global rime, session_id, box, STATUS_TOOLTIP 87 | if box && HasMethod(box, "Hide") 88 | box.Hide() 89 | rime.clear_composition(session_id) 90 | Suspend(-1) 91 | UpdateTrayTip() 92 | UpdateTrayIcon() 93 | if RabbitConfig.show_tips { 94 | ToolTip(A_IsSuspended ? "禁用" : "启用", , , STATUS_TOOLTIP) 95 | SetTimer(() => ToolTip(, , , STATUS_TOOLTIP), -RabbitConfig.show_tips_time) 96 | } 97 | SetupTrayMenu() 98 | } 99 | 100 | ClickHandler(wParam, lParam, msg, hWnd) { 101 | if !rime || !IsSet(session_id) || !session_id || A_IsSuspended 102 | return 103 | if lParam == WM_LBUTTONDOWN { 104 | RabbitGlobals.on_tray_icon_click := true 105 | } else if lParam == WM_LBUTTONUP { 106 | local old_ascii_mode := rime.get_option(session_id, "ascii_mode") 107 | rime.set_option(session_id, "ascii_mode", !old_ascii_mode) 108 | local new_ascii_mode := rime.get_option(session_id, "ascii_mode") 109 | if IsSet(UpdateWinAscii) { 110 | UpdateWinAscii(new_ascii_mode, true, RabbitGlobals.active_win, true) 111 | } 112 | status_text := new_ascii_mode ? ASCII_MODE_TRUE_LABEL_ABBR : ASCII_MODE_FALSE_LABEL_ABBR 113 | if RabbitConfig.show_tips { 114 | ToolTip(status_text, , , STATUS_TOOLTIP) 115 | SetTimer(() => ToolTip(, , , STATUS_TOOLTIP), -RabbitConfig.show_tips_time) 116 | } 117 | WinActivate("ahk_exe " . RabbitGlobals.active_win) 118 | RabbitGlobals.on_tray_icon_click := false 119 | } 120 | } 121 | 122 | UpdateTrayTip(schema_name := TRAY_SCHEMA_NAME, ascii_mode := TRAY_ASCII_MODE, full_shape := TRAY_FULL_SHAPE, ascii_punct := TRAY_ASCII_PUNCT) { 123 | global TRAY_SCHEMA_NAME, TRAY_ASCII_MODE, TRAY_FULL_SHAPE, TRAY_ASCII_PUNCT 124 | TRAY_SCHEMA_NAME := schema_name ? schema_name : TRAY_SCHEMA_NAME 125 | TRAY_ASCII_MODE := !!ascii_mode 126 | TRAY_FULL_SHAPE := !!full_shape 127 | TRAY_ASCII_PUNCT := !!ascii_punct 128 | local ss := A_IsSuspended ? "(已禁用)" : "" 129 | A_IconTip := Format( 130 | "玉兔毫 {} {}`n左键切换模式,右键打开菜单`n{} | {} | {}", ss, TRAY_SCHEMA_NAME, 131 | (TRAY_ASCII_MODE ? ASCII_MODE_TRUE_LABEL : ASCII_MODE_FALSE_LABEL), 132 | (TRAY_FULL_SHAPE ? FULL_SHAPE_TRUE_LABEL : FULL_SHAPE_FALSE_LABEL), 133 | (TRAY_ASCII_PUNCT ? ASCII_PUNCT_TRUE_LABEL : ASCII_PUNCT_FALSE_LABEL) 134 | ) 135 | } 136 | 137 | UpdateTrayIcon() { 138 | global TRAY_ASCII_MODE 139 | icon_path := RabbitGlobals.current_schema_icon 140 | if !IsSet(icon_path) || !icon_path 141 | icon_path := "Lib\rabbit.ico" 142 | if A_IsCompiled { 143 | icon_num := IN_MAINTENANCE ? 3 : (TRAY_ASCII_MODE ? 2 : (RabbitGlobals.current_schema_icon ? 0 : 1)) 144 | if icon_num { 145 | TraySetIcon(A_ScriptFullPath, icon_num) 146 | } else { 147 | TraySetIcon(RabbitGlobals.current_schema_icon) 148 | } 149 | } else 150 | TraySetIcon((A_IsSuspended || IN_MAINTENANCE) ? "Lib\rabbit-alt.ico" : (TRAY_ASCII_MODE ? "Lib\rabbit-ascii.ico" : icon_path), , true) 151 | } 152 | 153 | CheckNewVersion() { 154 | if !IsDigit(SubStr(RABBIT_VERSION, 1, 1)) { 155 | MsgBox("非正式版本,请前往仓库检查新版本", "玉兔毫输入法") 156 | return 157 | } 158 | 159 | http := ComObject("WinHttp.WinHttpRequest.5.1") 160 | url := "https://api.github.com/repos/rimeinn/rabbit/releases/latest" 161 | local ver := "" 162 | try { 163 | http.Open("GET", url, true) 164 | http.SetRequestHeader("Accept", "application/vnd.github+json") 165 | http.SetRequestHeader("X-GitHub-Api-Version", "2022-11-28") 166 | http.SetRequestHeader("User-Agent", "AutoHotkey") 167 | 168 | http.Send() 169 | http.WaitForResponse() 170 | 171 | status := http.Status 172 | if (status != 200) { 173 | MsgBox("无法获取最新版本信息,请检查网络连接", "玉兔毫输入法") 174 | return 175 | } 176 | 177 | responseText := http.ResponseText 178 | if RegExMatch(responseText, '"name"\s*:\s*"(.*?)"', &match) { 179 | if SubStr(match[1], 1, 1) == "v" 180 | ver := SubStr(match[1], 2) 181 | else 182 | ver := match[1] 183 | } else { 184 | MsgBox("无法解析版本字段,请稍后再试", "玉兔毫输入法") 185 | return 186 | } 187 | } 188 | 189 | if ver == "" { 190 | MsgBox("无法获取最新版本号,请稍后再试", "玉兔毫输入法") 191 | return 192 | } 193 | 194 | if VerCompare(ver, RABBIT_VERSION) > 0 { 195 | down := MsgBox(Format("发现新版本:{}`r`n是否前往下载?", ver), "玉兔毫输入法", "YesNo") 196 | if down == "Yes" { 197 | arch := A_Is64BitOS ? "x64" : "x86" 198 | Run(Format("https://github.com/rimeinn/rabbit/releases/download/v{1}/rabbit-v{1}-{2}.zip", ver, arch)) 199 | } 200 | } else { 201 | MsgBox("当前已是最新版本", "玉兔毫输入法") 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Commit CI 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | prepare-autohotkey-binaries: 13 | name: Prepare AutoHotkey Binaries 14 | runs-on: windows-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Install Dependencies 20 | uses: MinoruSekine/setup-scoop@v4.0.1 21 | with: 22 | buckets: extras 23 | apps: rcedit autohotkey imagemagick 24 | 25 | - name: Prepare binaries 26 | run: | 27 | Push-Location assets 28 | # https://learn.microsoft.com/windows/apps/design/style/iconography/app-icon-construction 29 | magick.exe -background transparent -define 'icon:auto-resize=16,24,32,48,256' zhuyin-t.svg rabbit.ico 30 | magick.exe -background transparent -define 'icon:auto-resize=16,24,32,48,256' zhuyin-t-alt.svg rabbit-alt.ico 31 | magick.exe -background transparent -define 'icon:auto-resize=16,24,32,48,256' pinyin-t.svg rabbit-ascii.ico 32 | Copy-Item "$(scoop prefix autohotkey)/v2/AutoHotkey32.exe","$(scoop prefix autohotkey)/v2/AutoHotkey64.exe" . 33 | rcedit.exe AutoHotkey32.exe --set-icon rabbit.ico 34 | rcedit.exe AutoHotkey64.exe --set-icon rabbit.ico 35 | Pop-Location 36 | Move-Item "assets/AutoHotkey32.exe","assets/AutoHotkey64.exe","assets/rabbit.ico","assets/rabbit-alt.ico","assets/rabbit-ascii.ico" . 37 | 38 | - name: Upload Icon 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: Icon 42 | path: | 43 | rabbit.ico 44 | rabbit-alt.ico 45 | rabbit-ascii.ico 46 | 47 | - name: Upload AutoHotkey 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: AutoHotkey 51 | path: | 52 | AutoHotkey32.exe 53 | AutoHotkey64.exe 54 | 55 | prepare-dependency: 56 | name: Prepare Dependency 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v4 61 | with: 62 | submodules: 'recursive' 63 | 64 | - name: Fetch Librime 65 | run: | 66 | WORK=`pwd` 67 | LIBRIME_TAG=$(curl -s https://api.github.com/repos/rime/librime/releases/latest | jq -r '.tag_name') 68 | LIBRIME_SHA=$(curl -s https://api.github.com/repos/rime/librime/tags | jq -r --arg LIBRIME_TAG "${LIBRIME_TAG}" '.[] | select(.name == $LIBRIME_TAG).commit.sha' | cut -c1-7) 69 | LIBRIME_MSVC_X86_URL="https://github.com/rime/librime/releases/download/${LIBRIME_TAG}/rime-${LIBRIME_SHA}-Windows-msvc-x86.7z" 70 | LIBRIME_MSVC_X86_DEPS_URL="https://github.com/rime/librime/releases/download/${LIBRIME_TAG}/rime-deps-${LIBRIME_SHA}-Windows-msvc-x86.7z" 71 | LIBRIME_MSVC_X64_URL="https://github.com/rime/librime/releases/download/${LIBRIME_TAG}/rime-${LIBRIME_SHA}-Windows-msvc-x64.7z" 72 | mkdir -p ${WORK}/librime-msvc ${WORK}/librime-clang 73 | cd ${WORK}/librime-msvc && \ 74 | wget -O librime.7z ${LIBRIME_MSVC_X86_URL} && \ 75 | 7z x '-i!dist/lib/rime.dll' librime.7z && \ 76 | cp dist/lib/rime.dll ${WORK}/rime-x86.dll && \ 77 | rm -rf librime.7z dist && \ 78 | wget -O deps.7z ${LIBRIME_MSVC_X86_DEPS_URL} && \ 79 | 7z x '-i!share/opencc' deps.7z && \ 80 | cp -r share/opencc ${WORK}/ && \ 81 | rm -rf deps.7z share 82 | cd ${WORK}/librime-clang && \ 83 | wget -O librime.7z ${LIBRIME_MSVC_X64_URL} && \ 84 | 7z x '-i!dist/lib/rime.dll' librime.7z && \ 85 | cp dist/lib/rime.dll ${WORK}/rime-x64.dll && \ 86 | rm -rf librime.7z dist 87 | 88 | - name: Upload Rime 89 | uses: actions/upload-artifact@v4 90 | with: 91 | name: Rime 92 | path: | 93 | rime-x86.dll 94 | rime-x64.dll 95 | 96 | - name: Prepare Data 97 | run: | 98 | WORK=`pwd` 99 | SCHEMAS=${WORK}/schemas 100 | plum_dir=${WORK}/plum 101 | rime_dir=${WORK}/Data 102 | rm -rf ${rime_dir} && mkdir -p ${rime_dir} 103 | rime_dir=${rime_dir} bash plum/rime-install 104 | cp ${SCHEMAS}/rabbit.yaml ${rime_dir}/ 105 | cp -r ${WORK}/opencc ${rime_dir}/ 106 | 107 | - name: Upload Data 108 | uses: actions/upload-artifact@v4 109 | with: 110 | name: Data 111 | path: Data 112 | 113 | build-rabbit: 114 | strategy: 115 | matrix: 116 | target: [ x86, x64 ] 117 | include: 118 | - { target: x86, ahk: AutoHotkey32.exe, rime: rime-x86.dll } 119 | - { target: x64, ahk: AutoHotkey64.exe, rime: rime-x64.dll } 120 | name: Build for ${{ matrix.target }} 121 | runs-on: ubuntu-latest 122 | needs: [ prepare-dependency, prepare-autohotkey-binaries ] 123 | steps: 124 | - name: Checkout 125 | uses: actions/checkout@v4 126 | with: 127 | submodules: 'recursive' 128 | 129 | - name: Download Dependencies 130 | uses: actions/download-artifact@v4 131 | 132 | - name: Copy Artifacts and Apply Patches 133 | run: | 134 | git apply --stat ./Lib/GetCaretPosEx/GetCaretPosEx.patch 135 | git apply --check ./Lib/GetCaretPosEx/GetCaretPosEx.patch 136 | git apply ./Lib/GetCaretPosEx/GetCaretPosEx.patch 137 | cp AutoHotkey/${{ matrix.ahk }} Rabbit.exe 138 | cp Rime/${{ matrix.rime }} ./Lib/librime-ahk/rime.dll 139 | cp Icon/* Lib/ 140 | cp plum/rime-install.bat . 141 | 142 | - name: Set Version 143 | run: | 144 | if [[ ${{ github.ref_name }} == v* ]]; then 145 | VER=${{ github.ref_name }} 146 | VER=${VER:1} 147 | else 148 | VER=${{ github.ref_name }}-$(git rev-parse --short ${{ github.sha }}) 149 | fi 150 | echo $VER 151 | sed -i -E 's/global RABBIT_VERSION := .+/global RABBIT_VERSION := \"'"$VER"'\"/' Lib/RabbitCommon.ahk 152 | 153 | - name: Upload Rabbit ${{ matrix.target }} 154 | uses: actions/upload-artifact@v4 155 | with: 156 | name: Rabbit-${{ matrix.target }} 157 | path: | 158 | Lib/Direct2D/Direct2D.ahk 159 | Lib/Direct2D/LICENSE 160 | Lib/librime-ahk/*.ahk 161 | Lib/librime-ahk/rime.dll 162 | Lib/librime-ahk/utils 163 | Lib/librime-ahk/LICENSE 164 | Lib/GetCaretPosEx/*.ahk 165 | Lib/GetCaretPosEx/LICENSE.txt 166 | Lib/*.ahk 167 | Lib/*.ico 168 | Rabbit.exe 169 | *.ahk 170 | LICENSE 171 | README.md 172 | rime-install.bat 173 | 174 | - name: Upload Full Zip of Rabbit ${{ matrix.target }} 175 | uses: actions/upload-artifact@v4 176 | with: 177 | name: Rabbit-Full-${{ matrix.target }} 178 | path: | 179 | Data 180 | Lib/Direct2D/Direct2D.ahk 181 | Lib/Direct2D/LICENSE 182 | Lib/librime-ahk/*.ahk 183 | Lib/librime-ahk/rime.dll 184 | Lib/librime-ahk/utils 185 | Lib/librime-ahk/LICENSE 186 | Lib/GetCaretPosEx/*.ahk 187 | Lib/GetCaretPosEx/LICENSE.txt 188 | Lib/*.ahk 189 | Lib/*.ico 190 | Rabbit.exe 191 | *.ahk 192 | LICENSE 193 | README.md 194 | rime-install.bat 195 | 196 | create-nightly: 197 | name: Create Nightly release 198 | if: ${{ github.ref == 'refs/heads/master' }} 199 | runs-on: ubuntu-latest 200 | needs: build-rabbit 201 | steps: 202 | - name: Checkout 203 | uses: actions/checkout@v4 204 | 205 | - name: Download x64 206 | uses: actions/download-artifact@v4 207 | with: 208 | name: Rabbit-Full-x64 209 | path: x64 210 | 211 | - name: Pack x64 212 | working-directory: x64 213 | run: | 214 | mkdir Rime && zip -r -q ../rabbit-nightly-x64.zip * 215 | 216 | - name: Download x86 217 | uses: actions/download-artifact@v4 218 | with: 219 | name: Rabbit-Full-x86 220 | path: x86 221 | 222 | - name: Pack x86 223 | working-directory: x86 224 | run: | 225 | mkdir Rime && zip -r -q ../rabbit-nightly-x86.zip * 226 | 227 | - name: Upload Nightly 228 | uses: marvinpinto/action-automatic-releases@latest 229 | with: 230 | repo_token: ${{ secrets.GITHUB_TOKEN }} 231 | automatic_release_tag: latest 232 | prerelease: true 233 | title: "Nightly" 234 | files: | 235 | rabbit-nightly-x64.zip 236 | rabbit-nightly-x86.zip 237 | 238 | create-release: 239 | name: Create Release 240 | if: startsWith(github.ref, 'refs/tags/v') 241 | runs-on: ubuntu-latest 242 | needs: build-rabbit 243 | steps: 244 | - name: Checkout 245 | uses: actions/checkout@v4 246 | 247 | - name: Create Release 248 | uses: softprops/action-gh-release@v2 249 | 250 | upload-release: 251 | strategy: 252 | matrix: 253 | target: [ x86, x64 ] 254 | name: Upload Release for ${{ matrix.target }} 255 | runs-on: ubuntu-latest 256 | needs: create-release 257 | steps: 258 | - name: Checkout 259 | uses: actions/checkout@v4 260 | 261 | - name: Download Artifacts 262 | uses: actions/download-artifact@v4 263 | with: 264 | name: Rabbit-Full-${{ matrix.target }} 265 | path: release 266 | 267 | - name: Pack Zip 268 | working-directory: release 269 | run: | 270 | mkdir Rime && zip -r -q ../rabbit-${{ github.ref_name }}-${{ matrix.target }}.zip * 271 | 272 | - name: Upload Assets 273 | uses: softprops/action-gh-release@v2 274 | with: 275 | prerelease: true 276 | files: | 277 | rabbit-${{ github.ref_name }}-${{ matrix.target }}.zip 278 | -------------------------------------------------------------------------------- /Lib/RabbitKeyTable.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, 2024 Xuesong Peng 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | class KeyDef { 20 | static mask := Map( 21 | "Shift", 1 << 0, 22 | "LShift", 1 << 0, 23 | "RShift", 1 << 0, 24 | "Ctrl", 1 << 2, 25 | "LCtrl", 1 << 2, 26 | "RCtrl", 1 << 2, 27 | "Alt", 1 << 3, 28 | "LAlt", 1 << 3, 29 | "RAlt", 1 << 3, 30 | "Win", 1 << 26, 31 | "LWin", 1 << 26, 32 | "RWin", 1 << 26, 33 | "Up", 1 << 30, 34 | ) 35 | 36 | static modifier_code := Map( 37 | "LShift", 0x00ffe1, 38 | "RShift", 0x00ffe2, 39 | "LCtrl", 0x00ffe3, 40 | "RCtrl", 0x00ffe4, 41 | "LAlt", 0x00ffe9, 42 | "RAlt", 0x00ffea, 43 | "LWin", 0x00ffeb, ; Super_L 44 | "RWin", 0x00ffec, ; Super_R 45 | ) 46 | 47 | static plain_keycode := Map( 48 | "'", 0x000027, 49 | ",", 0x00002c, 50 | "-", 0x00002d, 51 | ".", 0x00002e, 52 | "/", 0x00002f, 53 | "0", 0x000030, 54 | "1", 0x000031, 55 | "2", 0x000032, 56 | "3", 0x000033, 57 | "4", 0x000034, 58 | "5", 0x000035, 59 | "6", 0x000036, 60 | "7", 0x000037, 61 | "8", 0x000038, 62 | "9", 0x000039, 63 | ";", 0x00003b, 64 | "=", 0x00003d, 65 | "[", 0x00005b, 66 | "\", 0x00005c, 67 | "]", 0x00005d, 68 | "``", 0x000060, 69 | "a", 0x000061, 70 | "b", 0x000062, 71 | "c", 0x000063, 72 | "d", 0x000064, 73 | "e", 0x000065, 74 | "f", 0x000066, 75 | "g", 0x000067, 76 | "h", 0x000068, 77 | "i", 0x000069, 78 | "j", 0x00006a, 79 | "k", 0x00006b, 80 | "l", 0x00006c, 81 | "m", 0x00006d, 82 | "n", 0x00006e, 83 | "o", 0x00006f, 84 | "p", 0x000070, 85 | "q", 0x000071, 86 | "r", 0x000072, 87 | "s", 0x000073, 88 | "t", 0x000074, 89 | "u", 0x000075, 90 | "v", 0x000076, 91 | "w", 0x000077, 92 | "x", 0x000078, 93 | "y", 0x000079, 94 | "z", 0x00007a, 95 | ) 96 | static shifted_keycode := Map( 97 | "!", 0x000021, 98 | "`"", 0x000022, 99 | "#", 0x000023, 100 | "$", 0x000024, 101 | "%", 0x000025, 102 | "&", 0x000026, 103 | "(", 0x000028, 104 | ")", 0x000029, 105 | "*", 0x00002a, 106 | "+", 0x00002b, 107 | ":", 0x00003a, 108 | "<", 0x00003c, 109 | ">", 0x00003e, 110 | "?", 0x00003f, 111 | "@", 0x000040, 112 | "A", 0x000041, 113 | "B", 0x000042, 114 | "C", 0x000043, 115 | "D", 0x000044, 116 | "E", 0x000045, 117 | "F", 0x000046, 118 | "G", 0x000047, 119 | "H", 0x000048, 120 | "I", 0x000049, 121 | "J", 0x00004a, 122 | "K", 0x00004b, 123 | "L", 0x00004c, 124 | "M", 0x00004d, 125 | "N", 0x00004e, 126 | "O", 0x00004f, 127 | "P", 0x000050, 128 | "Q", 0x000051, 129 | "R", 0x000052, 130 | "S", 0x000053, 131 | "T", 0x000054, 132 | "U", 0x000055, 133 | "V", 0x000056, 134 | "W", 0x000057, 135 | "X", 0x000058, 136 | "Y", 0x000059, 137 | "Z", 0x00005a, 138 | "^", 0x00005e, 139 | "_", 0x00005f, 140 | "{", 0x00007b, 141 | "|", 0x00007c, 142 | "}", 0x00007d, 143 | "~", 0x00007e, 144 | ) 145 | static other_keycode := Map( 146 | "Space", 0x000020, 147 | "Backspace", 0x00ff08, 148 | "Tab", 0x00ff09, 149 | "Enter", 0x00ff0d, ; Return 150 | ; "Pause", 0x00ff13, 151 | ; "ScrollLock", 0x00ff14, 152 | "Escape", 0x00ff1b, 153 | "Home", 0x00ff50, 154 | "Left", 0x00ff51, 155 | "Up", 0x00ff52, 156 | "Right", 0x00ff53, 157 | "Down", 0x00ff54, 158 | "PgUp", 0x00ff55, 159 | "PgDn", 0x00ff56, 160 | "End", 0x00ff57, 161 | ; "Insert", 0x00ff63, 162 | ; "AppsKey", 0x00ff67, ; Menu 163 | ; "Help", 0x00ff6a, 164 | ; "NumLock", 0x00ff7f, 165 | "NumpadEnter", 0x00ff8d, 166 | "NumpadHome", 0x00ff95, 167 | "NumpadLeft", 0x00ff96, 168 | "NumpadUp", 0x00ff97, 169 | "NumpadRight", 0x00ff98, 170 | "NumpadDown", 0x00ff99, 171 | "NumpadPgUp", 0x00ff9a, 172 | "NumpadPgDn", 0x00ff9b, 173 | "NumpadEnd", 0x00ff9c, 174 | ; "NumpadIns", 0x00ff9e, 175 | "NumpadDel", 0x00ff9f, 176 | "NumpadMult", 0x00ffaa, 177 | "NumpadAdd", 0x00ffab, 178 | "NumpadSub", 0x00ffad, 179 | "NumpadDot", 0x00ffae, 180 | "NumpadDiv", 0x00ffaf, 181 | "Numpad0", 0x00ffb0, 182 | "Numpad1", 0x00ffb1, 183 | "Numpad2", 0x00ffb2, 184 | "Numpad3", 0x00ffb3, 185 | "Numpad4", 0x00ffb4, 186 | "Numpad5", 0x00ffb5, 187 | "Numpad6", 0x00ffb6, 188 | "Numpad7", 0x00ffb7, 189 | "Numpad8", 0x00ffb8, 190 | "Numpad9", 0x00ffb9, 191 | "F1", 0x00ffbe, 192 | "F2", 0x00ffbf, 193 | "F3", 0x00ffc0, 194 | "F4", 0x00ffc1, 195 | "F5", 0x00ffc2, 196 | "F6", 0x00ffc3, 197 | "F7", 0x00ffc4, 198 | "F8", 0x00ffc5, 199 | "F9", 0x00ffc6, 200 | "F10", 0x00ffc7, 201 | "F11", 0x00ffc8, 202 | "F12", 0x00ffc9, 203 | "F13", 0x00ffca, 204 | "F14", 0x00ffcb, 205 | "F15", 0x00ffcc, 206 | "F16", 0x00ffcd, 207 | "F17", 0x00ffce, 208 | "F18", 0x00ffcf, 209 | "F19", 0x00ffd0, 210 | "F20", 0x00ffd1, 211 | "F21", 0x00ffd2, 212 | "F22", 0x00ffd3, 213 | "F23", 0x00ffd4, 214 | "F24", 0x00ffd5, 215 | ; "CapsLock", 0x00ffe5, 216 | "Delete", 0x00ffff, 217 | ) 218 | 219 | static rime_to_ahk := Map( 220 | "apostrophe", "'", 221 | "quoteright", "'", 222 | "comma", ",", 223 | "minus", "-", 224 | "period", ".", 225 | "slash", "/", 226 | "semicolon", ";", 227 | "equal", "=", 228 | "bracketleft", "[", 229 | "backslash", "\", 230 | "bracketright", "]", 231 | "grave", "``", 232 | "quoteleft", "``", 233 | "exclam", "!", 234 | "quotedbl", "`"", 235 | "numbersign", "#", 236 | "dollar", "$", 237 | "persent", "%", 238 | "ampersand", "&", 239 | "parenleft", "(", 240 | "parenright", ")", 241 | "asterisk", "*", 242 | "plus", "+", 243 | "colon", ":", 244 | "less", "<", 245 | "greater", ">", 246 | "question", "?", 247 | "at", "@", 248 | "asciicircum", "^", 249 | "underscore", "_", 250 | "braceleft", "{", 251 | "bar", "|", 252 | "braceright", "}", 253 | "asciitilde", "~", 254 | "space", "Space", 255 | "BackSpace", "Backspace", 256 | "Return", "Enter", 257 | "Scroll_Lock", "ScrollLock", 258 | "Page_Up", "PgUp", 259 | "Page_Down", "PgDn", 260 | "Menu", "AppsKey", 261 | "Num_Lock", "NumLock", 262 | "KP_Enter", "NumpadEnter", 263 | "KP_Home", "NumpadHome", 264 | "KP_Left", "NumpadLeft", 265 | "KP_Up", "NumpadUp", 266 | "KP_Right", "NumpadRight", 267 | "KP_Down", "NumpadDown", 268 | "KP_Page_Up", "NumpadPgUp", 269 | "KP_Page_Down", "NumpadPgDn", 270 | "KP_End", "NumpadEnd", 271 | "KP_Insert", "NumpadIns", 272 | "KP_Delete", "NumpadDel", 273 | "KP_Multiply", "NumpadMult", 274 | "KP_Add", "NumpadAdd", 275 | "KP_Subtract", "NumpadSub", 276 | "KP_Decimal", "NumpadDot", 277 | "KP_Divide", "NumpadDiv", 278 | "KP_0", "Numpad0", 279 | "KP_1", "Numpad1", 280 | "KP_2", "Numpad2", 281 | "KP_3", "Numpad3", 282 | "KP_4", "Numpad4", 283 | "KP_5", "Numpad5", 284 | "KP_6", "Numpad6", 285 | "KP_7", "Numpad7", 286 | "KP_8", "Numpad8", 287 | "KP_9", "Numpad9", 288 | "Caps_Lock", "CapsLock", 289 | ) 290 | } 291 | -------------------------------------------------------------------------------- /Lib/RabbitUIStyle.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Xuesong Peng 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | #Include 20 | 21 | class UIStyle { 22 | static use_dark := false 23 | static font_face := "Microsoft YaHei UI" 24 | static label_font_face := "Microsoft YaHei UI" 25 | static comment_font_face := "Microsoft YaHei UI" 26 | static font_point := 14 27 | static label_font_point := 14 28 | static comment_font_point := 14 29 | static label_format := "{}. " 30 | 31 | static border_width := 2 32 | static corner_radius := 6 33 | static round_corner := 4 34 | static margin_x := 6 35 | static margin_y := 6 36 | static min_width := 160 37 | 38 | static border_color := 0xffe0e0e0 39 | static text_color := 0xff000000 40 | static back_color := 0xffeeeeec 41 | static candidate_text_color := 0xff000000 42 | static candidate_back_color := 0xffeeeeec 43 | static label_color := 0xff000000 44 | static comment_text_color := 0xff000000 45 | static hilited_text_color := 0xff000000 46 | static hilited_back_color := 0xffd4d4d4 47 | static hilited_candidate_text_color := 0xffffffff 48 | static hilited_candidate_back_color := 0xff0a3afa 49 | static hilited_label_color := 0xffffffff 50 | static hilited_comment_text_color := 0xff000000 51 | 52 | static Update(config, initialize) { 53 | global rime 54 | if !rime || !config 55 | return 56 | UIStyle.use_dark := false 57 | UIStyle.font_face := rime.config_get_string(config, "style/font_face") 58 | if not UIStyle.font_face 59 | UIStyle.font_face := "Microsoft YaHei UI" 60 | UIStyle.label_font_face := rime.config_get_string(config, "style/label_font_face") 61 | if not UIStyle.label_font_face 62 | UIStyle.label_font_face := "Microsoft YaHei UI" 63 | UIStyle.comment_font_face := rime.config_get_string(config, "style/comment_font_face") 64 | if not UIStyle.comment_font_face 65 | UIStyle.comment_font_face := "Microsoft YaHei UI" 66 | UIStyle.font_point := rime.config_get_int(config, "style/font_point") 67 | if UIStyle.font_point <= 0 68 | UIStyle.font_point := 14 69 | UIStyle.label_font_point := rime.config_get_int(config, "style/label_font_point") 70 | if UIStyle.label_font_point <= 0 71 | UIStyle.label_font_point := 14 72 | UIStyle.comment_font_point := rime.config_get_int(config, "style/comment_font_point") 73 | if UIStyle.comment_font_point <= 0 74 | UIStyle.comment_font_point := 14 75 | if rime.config_test_get_string(config, "style/label_format", &fmt) && fmt 76 | UIStyle.label_format := fmt 77 | if rime.config_test_get_int(config, "style/layout/corner_radius", &cr) && cr >= 0 78 | UIStyle.corner_radius := cr 79 | if rime.config_test_get_int(config, "style/layout/round_corner", &r) && r >= 0 80 | UIStyle.round_corner := r 81 | if rime.config_test_get_int(config, "style/layout/margin_x", &mx) && mx >= 0 82 | UIStyle.margin_x := mx 83 | if rime.config_test_get_int(config, "style/layout/margin_y", &my) && my >= 0 84 | UIStyle.margin_y := my 85 | if rime.config_test_get_int(config, "style/layout/min_width", &w) && w >= 0 86 | UIStyle.min_width := w 87 | if initialize and color := rime.config_get_string(config, "style/color_scheme") 88 | UIStyle.UpdateColor(config, color) 89 | } 90 | 91 | static UpdateColor(config, color) { 92 | global rime 93 | if color or (buffer := rime.config_get_string(config, "style/color_scheme")) { 94 | local prefix := "preset_color_schemes/" . (color ? color : buffer) 95 | local fmt := "argb" ; different from Weasel 96 | if cfmt := rime.config_get_string(config, prefix . "/color_format") { 97 | if cfmt = "argb" or cfmt = "rgba" or cfmt = "abgr" 98 | fmt := cfmt 99 | } 100 | 101 | UIStyle.border_color := UIStyle.GetColor(config, prefix . "/border_color", fmt, 0xffe0e0e0) 102 | UIStyle.text_color := UIStyle.GetColor(config, prefix . "/text_color", fmt, 0xff000000) 103 | UIStyle.back_color := UIStyle.GetColor(config, prefix . "/back_color", fmt, 0xffeceeee) 104 | UIStyle.candidate_text_color := UIStyle.GetColor(config, prefix . "/candidate_text_color", fmt, UIStyle.text_color) 105 | UIStyle.candidate_back_color := UIStyle.GetColor(config, prefix . "/candidate_back_color", fmt, UIStyle.back_color) 106 | UIStyle.label_color := UIStyle.GetColor(config, prefix . "/label_color", fmt, UIStyle.BlendColors(UIStyle.candidate_text_color, UIStyle.candidate_back_color)) 107 | UIStyle.comment_text_color := UIStyle.GetColor(config, prefix . "/comment_text_color", fmt, UIStyle.label_color) 108 | UIStyle.hilited_text_color := UIStyle.GetColor(config, prefix . "/hilited_text_color", fmt, UIStyle.text_color) 109 | UIStyle.hilited_back_color := UIStyle.GetColor(config, prefix . "/hilited_back_color", fmt, UIStyle.back_color) 110 | UIStyle.hilited_candidate_text_color := UIStyle.GetColor(config, prefix . "/hilited_candidate_text_color", fmt, UIStyle.hilited_text_color) 111 | UIStyle.hilited_candidate_back_color := UIStyle.GetColor(config, prefix . "/hilited_candidate_back_color", fmt, UIStyle.hilited_back_color) 112 | UIStyle.hilited_label_color := UIStyle.GetColor(config, prefix . "/hilited_label_color", fmt, UIStyle.BlendColors(UIStyle.hilited_candidate_text_color, UIStyle.hilited_candidate_back_color)) 113 | UIStyle.hilited_comment_text_color := UIStyle.GetColor(config, prefix . "/hilited_comment_text_color", fmt, UIStyle.hilited_label_color) 114 | 115 | return true 116 | } 117 | return false 118 | } 119 | 120 | static BlendColors(fcolor, bcolor) { 121 | local fA := (fcolor >> 24) & 0xff 122 | if fA == 0xff 123 | return fcolor 124 | local fR := (fcolor >> 16) & 0xff 125 | local fG := (fcolor >> 8) & 0xff 126 | local fB := fcolor & 0xff 127 | local bA := (bcolor >> 24) & 0xff 128 | local bR := (bcolor >> 16) & 0xff 129 | local bG := (bcolor >> 8) & 0xff 130 | local bB := bcolor & 0xff 131 | 132 | local fAlpha := fA / 255.0 133 | local bAlpha := bA / 255.0 134 | 135 | local retAlpha := fAlpha + bAlpha * (1 - fAlpha) 136 | 137 | local retR := Integer((fR * fAlpha + bR * bAlpha * (1 - fAlpha)) / retAlpha) 138 | local retG := Integer((fG * fAlpha + bG * bAlpha * (1 - fAlpha)) / retAlpha) 139 | local retB := Integer((fB * fAlpha + bB * bAlpha * (1 - fAlpha)) / retAlpha) 140 | 141 | return (Integer(retAlpha) * 255 << 24) | (retR << 16) | (retG << 8) | retB 142 | } 143 | 144 | static GetColor(config, key, fmt, fallback) { 145 | global rime 146 | if not rime.config_test_get_string(config, key, &color) 147 | return fallback 148 | 149 | local val := fallback 150 | make_opaque() { 151 | val := (fmt != "rgba") ? (val | 0xff000000) : ((val << 8) | 0x000000ff) 152 | } 153 | convert_color_to_argb(clr, format) { 154 | if format = "argb" 155 | return clr & 0xffffffff 156 | else if format = "abgr" 157 | return ((clr & 0x00ff0000) >> 16) | (clr & 0x0000ff00) | ((clr & 0x000000ff) << 16) | (clr & 0xff000000) 158 | else if format = "rgba" 159 | return ((clr & 0x00ff00) << 8) | (clr & 0xff0000) | ((clr & 0x0000ff) >> 8) | (clr & 0xff000000) 160 | else 161 | return clr & 0xffffffff 162 | } 163 | 164 | if RegExMatch(color, "i)^0x[0-9a-f]+$") { 165 | tmp := SubStr(RegExReplace(color, "i)0x"), 1, 8) 166 | switch StrLen(tmp) { 167 | case 6: 168 | val := Integer("0x" . tmp) 169 | make_opaque() 170 | case 3: 171 | tmp := Format( 172 | "{1}{1}{2}{2}{3}{3}", 173 | SubStr(tmp, 1, 1), 174 | SubStr(tmp, 2, 1), 175 | SubStr(tmp, 3, 1) 176 | ) 177 | val := Integer("0x" . tmp) 178 | make_opaque() 179 | case 4: 180 | tmp := Format( 181 | "{1}{1}{2}{2}{3}{3}{4}{4}", 182 | SubStr(tmp, 1, 1), 183 | SubStr(tmp, 2, 1), 184 | SubStr(tmp, 3, 1), 185 | SubStr(tmp, 4, 1) 186 | ) 187 | val := Integer("0x" . tmp) 188 | case 8: 189 | val := Integer("0x" . tmp) 190 | default: 191 | return fallback 192 | } 193 | } else { 194 | tmp := 0 195 | if not rime.config_test_get_int(config, key, &tmp) 196 | return fallback 197 | val := tmp 198 | make_opaque() 199 | } 200 | return convert_color_to_argb(val, fmt) 201 | } 202 | } 203 | 204 | RabbitIsUserDarkMode() { 205 | try { 206 | local data := RegRead("HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme") 207 | } 208 | if IsSet(data) && IsInteger(data) { 209 | return !data 210 | } 211 | return false 212 | } 213 | 214 | OnColorChange(wParam, lParam, msg, hWnd) { 215 | global rime, IS_DARK_MODE, box 216 | local old_dark := IS_DARK_MODE 217 | IS_DARK_MODE := RabbitIsUserDarkMode() 218 | if old_dark != IS_DARK_MODE { 219 | if config := rime.config_open("rabbit") { 220 | UIStyle.Update(config, true) 221 | if IS_DARK_MODE { 222 | if color_name := rime.config_get_string(config, "style/color_scheme_dark") 223 | UIStyle.use_dark := UIStyle.UpdateColor(config, color_name) 224 | } 225 | 226 | rime.config_close(config) 227 | box.UpdateUIStyle() 228 | } 229 | DarkMode.set(IS_DARK_MODE) 230 | } 231 | } 232 | 233 | ; https://www.autohotkey.com/boards/viewtopic.php?p=515002&sid=859605067314b6d823a026658547b66f#p515002 234 | class DarkMode { 235 | static set(mode) { 236 | DllCall(DllCall("GetProcAddress", "ptr", DllCall("GetModuleHandle", "str", "uxtheme", "ptr"), "ptr", 135, "ptr"), "int", mode) 237 | DllCall(DllCall("GetProcAddress", "ptr", DllCall("GetModuleHandle", "str", "uxtheme", "ptr"), "ptr", 136, "ptr")) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /Lib/RabbitThemesUI.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2005 Tim 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | #Include 20 | #Include 21 | 22 | class CandidatePreview { 23 | hBitmap := 0 24 | 25 | __New(ctrl) { 26 | this.imgCtrl := ctrl 27 | this.d2d := Direct2D() 28 | this.dpiScale := this.d2d.GetDesktopDpiScale() 29 | } 30 | 31 | __Delete() { 32 | if this.hBitmap 33 | DllCall("DeleteObject", "UPtr", this.hBitmap), this.hBitmap := 0 34 | } 35 | 36 | Build(theme, &calcW, &calcH) { 37 | this.borderWidth := UIStyle.border_width 38 | this.borderColor := UIStyle.border_color 39 | this.boxCornerR := UIStyle.corner_radius 40 | this.hlCornerR := UIStyle.round_corner 41 | this.lineSpacing := UIStyle.margin_y 42 | this.padding := UIStyle.margin_x 43 | 44 | ; only use one font to preview 45 | this.fontName := theme.HasOwnProp("font_face") ? theme.font_face : UIStyle.font_face 46 | this.fontSize := theme.HasOwnProp("font_point") ? theme.font_point : UIStyle.font_point 47 | this.fontSize *= (em2pt := (96.0 / 72.0)) 48 | ; preedite style 49 | this.borderColor := theme.border_color 50 | this.textColor := theme.text_color 51 | this.backgroundColor := theme.back_color 52 | this.hlTxtColor := theme.hilited_text_color 53 | this.hlBgColor := theme.hilited_back_color 54 | ; candidate style 55 | this.hlCandTxtColor := theme.hilited_candidate_text_color 56 | this.hlCandBgColor := theme.hilited_candidate_back_color 57 | this.candTxtColor := theme.candidate_text_color 58 | this.candBgColor := theme.candidate_back_color 59 | 60 | this.prdSelSize := this.d2d.GetMetrics("RIME", this.fontName, this.fontSize) 61 | this.prdHlSize := this.d2d.GetMetrics("shu ru fa", this.fontName, this.fontSize) 62 | this.candSize := this.d2d.GetMetrics("1. 输入法", this.fontName, this.fontSize) 63 | this.maxRowWidth := this.prdSelSize.w + this.padding + this.prdHlSize.w 64 | this.previewWidth := Ceil(this.maxRowWidth) + this.padding * 2 + this.borderWidth * 2 65 | this.previewHeight := Ceil((this.candSize.h + this.lineSpacing) * 6) + this.lineSpacing * 2 + this.borderWidth * 2 - this.lineSpacing ; Remove last line spacing 66 | calcW := this.previewWidth 67 | calcH := this.previewHeight 68 | } 69 | 70 | Render(candsArray, selIndex) { 71 | d2d1WicRt := this.d2d.SetRenderTarget("wic", this.previewWidth, this.previewHeight) 72 | this.d2d.BeginDraw() 73 | 74 | if (this.borderWidth > 0) { 75 | ; Draw outer border as filled rounded rectangle (border color) 76 | this.d2d.FillRoundedRectangle(0, 0, this.previewWidth, this.previewHeight, this.boxCornerR, this.boxCornerR, this.borderColor) 77 | ; Draw inner background next 78 | bgX := this.borderWidth, bgY := this.borderWidth 79 | bgW := this.previewWidth - this.borderWidth * 2 80 | bgH := this.previewHeight - this.borderWidth * 2 81 | bgR := this.boxCornerR > this.borderWidth ? this.boxCornerR - this.borderWidth : 0 82 | this.d2d.FillRoundedRectangle(bgX, bgY, bgW, bgH, bgR, bgR, this.backgroundColor) 83 | } else { 84 | this.d2d.FillRoundedRectangle(0, 0, this.previewWidth, this.previewHeight, this.boxCornerR, this.boxCornerR, this.backgroundColor) 85 | } 86 | 87 | ; Draw preedit 88 | currentY := this.padding + this.borderWidth 89 | prdSelTextRect := {text: "RIME", x: this.padding + this.borderWidth, y: currentY, w: this.prdSelSize.w, h: this.prdSelSize.h } 90 | prdHlTextRect := {text: "shu ru fa", x: this.padding + this.borderWidth + this.padding + this.prdSelSize.w, y: currentY, w: this.prdHlSize.w, h: this.prdHlSize.h } 91 | ; highlight background for preedit selection 92 | this.d2d.FillRoundedRectangle(prdHlTextRect.x, prdHlTextRect.y, prdHlTextRect.w, prdHlTextRect.h, 93 | this.hlCornerR, this.hlCornerR, this.hlBgColor) 94 | this.d2d.DrawText(prdSelTextRect.text, prdSelTextRect.x, prdSelTextRect.y, this.fontSize, this.textColor, this.fontName) 95 | this.d2d.DrawText(prdHlTextRect.text, prdHlTextRect.x, prdHlTextRect.y, this.fontSize, this.hlTxtColor, this.fontName) 96 | currentY += Max(this.prdSelSize.h, this.prdHlSize) + this.lineSpacing 97 | 98 | 99 | ; Draw candidates 100 | for i, candidate in candsArray { 101 | candFg := this.candTxtColor 102 | if (A_Index == selIndex) { ; Draw highlight if selected 103 | candFg := this.hlCandTxtColor 104 | highlightX := this.borderWidth + this.padding / 2 105 | highlightY := currentY - this.lineSpacing / 2 106 | highlightW := this.previewWidth - this.borderWidth * 2 - this.padding 107 | highlightH := this.candSize.h + this.lineSpacing 108 | this.d2d.FillRoundedRectangle(highlightX, highlightY, highlightW, highlightH, this.hlCornerR, this.hlCornerR, this.hlCandBgColor) 109 | } 110 | 111 | textToDraw := i . ". " . candidate 112 | candidateRowRect := { x: this.padding + this.borderWidth, y: currentY, w: this.maxRowWidth, h: this.candSize.h } 113 | this.d2d.DrawText(textToDraw, candidateRowRect.x, candidateRowRect.y, this.fontSize, candFg, this.fontName) 114 | currentY += this.candSize.h + this.lineSpacing 115 | } 116 | this.d2d.EndDraw() 117 | 118 | if this.hBitmap := d2d1WicRt.GetHBitmapFromWICBitmap() { 119 | ; Replace preview image with hBitmap 120 | SendMessage(STM_SETIMAGE := 0x0172, IMAGE_BITMAP := 0, this.hBitmap, this.imgCtrl.Hwnd) 121 | DllCall("DeleteObject", "UPtr", this.hBitmap) 122 | this.d2d.Clear() 123 | } 124 | } 125 | } 126 | 127 | class ThemesGUI { 128 | __New(result) { 129 | this.result := result 130 | this.preset_color_schemes := Map() 131 | this.colorSchemeMap := Map() 132 | this.previewFontName := UIStyle.font_face 133 | this.previewFontSize := UIStyle.font_point 134 | this.themeListBoxW := 400 135 | this.previewGroupW := 300 136 | this.previewGroupH := 418 137 | this.previewGroupOffset := 20 138 | this.currentTheme := "aqua" 139 | this.candsArray := ["输入法", "输入", "数", "书", "输"] 140 | this.gui := Gui("+LastFound +OwnDialogs -DPIScale +AlwaysOnTop", "选择主题") 141 | this.gui.MarginX := 10 142 | this.gui.MarginY := 10 143 | this.gui.SetFont("s10", "Microsoft YaHei UI") 144 | this.Build() 145 | } 146 | 147 | Build() { 148 | this.preset_color_schemes := this.GetPresetStylesMap() 149 | local colorChoices := [] 150 | for key, preset in this.preset_color_schemes { 151 | colorChoices.Push(preset["name"]) 152 | this.colorSchemeMap[preset["name"]] := key 153 | } 154 | this.gui.Add("Text", "x10 y10", "主题:").GetPos(, , , &titleH) 155 | this.titleH := titleH 156 | 157 | this.themeListBox := this.gui.AddListBox("r15 w" . this.themeListBoxW . " -Multi", colorChoices) 158 | this.themeListBox.Choose(1) 159 | this.themeListBox.OnEvent("Change", this.OnChangeColorScheme.Bind(this)) 160 | this.gui.AddGroupBox(Format("x+{:d} yp-8 w{:d} h{:d} Section", this.previewGroupOffset, this.previewGroupW, this.previewGroupH), "预览") 161 | ; 0xE(SS_BITMAP) or 0x4E (Bitmap and Resizable, but text is unclear) 162 | this.previewImg := this.gui.AddPicture("xp+50 yp+50 w180 h300 0xE BackgroundWhite") 163 | this.candidateBox := CandidatePreview(this.previewImg) 164 | 165 | this.currentTheme := this.colorSchemeMap[this.themeListBox.Text] 166 | this.SetPreviewCandsBox(this.currentTheme, this.previewFontName, this.previewFontSize) 167 | 168 | this.setFontBtn := this.gui.AddButton("x10 ys+440 w160", "设置字体") 169 | this.confirmBtn := this.gui.AddButton("x+400 ys+440 w160", "确定") 170 | this.setFontBtn.OnEvent("Click", this.OnSetFont.Bind(this)) 171 | this.confirmBtn.OnEvent("Click", this.OnConfirm.Bind(this)) 172 | } 173 | 174 | Show() { 175 | this.gui.Show("AutoSize") 176 | } 177 | 178 | OnChangeColorScheme(ctrl, info) { 179 | if !this.colorSchemeMap.Has(ctrl.Text) 180 | return 181 | 182 | this.currentTheme := this.colorSchemeMap[ctrl.Text] 183 | this.SetPreviewCandsBox(this.currentTheme, this.previewFontName, this.previewFontSize) 184 | } 185 | 186 | OnSetFont(*) { 187 | fontGui := Gui("AlwaysOnTop +Owner" this.gui.Hwnd, "字体选择") 188 | fontGui.SetFont("s10") 189 | 190 | fontGui.AddText("x10 y10", "字体名称:") 191 | fontChoice := fontGui.AddDropDownList("x+10 yp-4 w180 hp r10", GUIUtilities.GetFontArray()) 192 | fontChoice.Text := this.previewFontName 193 | 194 | fontGui.AddText("x+30 y10", "大小:") 195 | fontSizeEdit := fontGui.Add("Edit", "x+0 yp-6 w60 Limit2 Number") 196 | fontGui.AddUpDown("Range10-20", this.previewFontSize) 197 | 198 | okBtn := fontGui.AddButton("x10 yp+30 w120", "确定") 199 | fontGui.AddButton("x+150 yp w120", "取消").OnEvent("Click", (*) => fontGui.Destroy()) 200 | 201 | okBtn.OnEvent("Click", (*) => ( 202 | this.previewFontName := fontChoice.Text, 203 | this.previewFontSize := fontSizeEdit.Value, 204 | this.SetPreviewCandsBox(this.currentTheme, this.previewFontName, this.previewFontSize), 205 | fontGui.Destroy() 206 | )) 207 | 208 | fontGui.Show() 209 | } 210 | 211 | OnConfirm(*) { 212 | global rime 213 | if rime and config := rime.config_open("rabbit") { 214 | rime.config_set_string(config, "style/color_scheme", this.currentTheme) 215 | rime.config_set_int(config, "style/font_point", this.previewFontSize) 216 | rime.config_set_string(config, "style/font_face", this.previewFontName) 217 | UIStyle.Update(config, init := true) 218 | rime.config_close(config) 219 | this.result.yes := true 220 | } 221 | 222 | this.gui.Hide() 223 | } 224 | 225 | SetPreviewCandsBox(theme, fontName, fontSize) { 226 | this.previewStyle := this.GetThemeColor(theme) 227 | this.previewStyle.font_face := fontName 228 | this.previewStyle.font_point := fontSize 229 | this.candidateBox.Build(this.previewStyle, &candidateBoxW, &candidateBoxH) 230 | previewCandsBoxX := this.gui.MarginX + this.themeListBoxW + this.previewGroupOffset + Round((this.previewGroupW - candidateBoxW) / 2) 231 | previewCandsBoxY := this.gui.MarginY + this.titleH + Round((this.previewGroupH - candidateBoxH) / 2) 232 | this.previewImg.Move(previewCandsBoxX, previewCandsBoxY, candidateBoxW, candidateBoxH) 233 | this.candidateBox.Render(this.candsArray, 1) 234 | } 235 | 236 | GetPresetStylesMap() { 237 | local presetStylesMap := Map() 238 | global rime 239 | if rime and config := rime.config_open("rabbit") { 240 | if iter := rime.config_begin_map(config, "preset_color_schemes") { 241 | while rime.config_next(iter) { 242 | styleMap := Map() 243 | theme := StrLower(iter.key) 244 | if name := rime.config_get_string(config, "preset_color_schemes/" . theme . "/name") { 245 | styleMap["name"] := name 246 | UIStyle.UpdateColor(config, theme) 247 | } 248 | styleMap["border_color"] := UIStyle.border_color 249 | styleMap["text_color"] := UIStyle.text_color 250 | styleMap["back_color"] := UIStyle.back_color 251 | styleMap["hilited_text_color"] := UIStyle.hilited_text_color 252 | styleMap["hilited_back_color"] := UIStyle.hilited_back_color 253 | styleMap["hilited_candidate_text_color"] := UIStyle.hilited_candidate_text_color 254 | styleMap["hilited_candidate_back_color"] := UIStyle.hilited_candidate_back_color 255 | styleMap["candidate_text_color"] := UIStyle.candidate_text_color 256 | styleMap["candidate_back_color"] := UIStyle.candidate_back_color 257 | presetStylesMap[theme] := styleMap 258 | } 259 | rime.config_end(iter) 260 | } 261 | ; restore UIStyle 262 | UIStyle.Update(config, init := true) 263 | rime.config_close(config) 264 | } 265 | return presetStylesMap 266 | } 267 | 268 | GetThemeColor(selTheme) { 269 | style := this.preset_color_schemes[selTheme] 270 | return { 271 | border_color: style["border_color"], 272 | text_color: style["text_color"], 273 | back_color: style["back_color"], 274 | hilited_text_color: style["hilited_text_color"], 275 | hilited_back_color: style["hilited_back_color"], 276 | hilited_candidate_text_color: style["hilited_candidate_text_color"], 277 | hilited_candidate_back_color: style["hilited_candidate_back_color"], 278 | candidate_text_color: style["candidate_text_color"], 279 | candidate_back_color: style["candidate_back_color"], 280 | } 281 | } 282 | } 283 | 284 | Class GUIUtilities { 285 | static GetFontArray() { 286 | static fontArr 287 | if isSet(fontArr) 288 | return fontArr 289 | 290 | sFont := Buffer(128, 0) 291 | NumPut("UChar", 1, sFont, 23) 292 | DllCall("EnumFontFamiliesEx", "ptr", DllCall("GetDC", "ptr", 0), "ptr", sFont.Ptr, "ptr", CallbackCreate(EnumFontProc), "ptr", ObjPtr(fontMap := Map()), "uint", 0) 293 | 294 | fontArr := Array() 295 | for key, value in fontMap 296 | fontArr.Push(SubStr(key, 2)) ; remove "@" 297 | return fontArr 298 | 299 | EnumFontProc(lpFont, lpntme, textFont, lParam) { 300 | font := StrGet(lpFont + 28, "UTF-16") 301 | ObjFromPtrAddRef(lParam)[font] := "" 302 | return true 303 | } 304 | } 305 | 306 | static GetMonitorDpiScale() { 307 | hr := DllCall( 308 | "Shcore.dll\GetDpiForMonitor", 309 | "ptr", hMonitor := DllCall("MonitorFromPoint", "int64", 0, "uint", 2, "ptr"), 310 | "int", MDT_EFFECTIVE_DPI := 0, 311 | "uint*", &dpiX := 0, 312 | "uint*", &dpiY := 0 313 | ) 314 | 315 | if (hr != 0) 316 | return 1 317 | 318 | return dpiX / 96 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /schemas/rabbit.yaml: -------------------------------------------------------------------------------- 1 | # Rabbit settings 2 | # encoding: utf-8 3 | 4 | config_version: "0.1" 5 | 6 | # Define the hotkey to suspend Rabbit 7 | suspend_hotkey: null 8 | 9 | show_tips: true 10 | # How long a period tips shows 11 | show_tips_time: 1200 12 | # 0: always send by clipboard 13 | # >0: send by clipboard if length >= value 14 | # <0: never send by clipboard 15 | send_by_clipboard_length: 8 16 | 17 | app_options: 18 | cmd.exe: 19 | ascii_mode: true 20 | conhost.exe: 21 | ascii_mode: true 22 | windowsterminal.exe: 23 | ascii_mode: true 24 | 25 | # Use global ascii mode 26 | global_ascii: false 27 | 28 | # Always show candidate box at top-left corner 29 | fix_candidate_box: false 30 | 31 | style: 32 | color_scheme: aqua 33 | font_face: Microsoft YaHei UI 34 | label_font_face: Microsoft YaHei UI 35 | comment_font_face: Microsoft YaHei UI 36 | font_point: 14 37 | label_font_point: 14 38 | comment_font_point: 14 39 | # 不同于小狼毫,格式化标签文本需符合 AutoHotkey 语法 40 | # 详见 https://wyagd001.github.io/v2/docs/lib/Format.htm#FormatSpec 41 | label_format: "{:s}. " 42 | 43 | # Copied from weasel.yaml 44 | # 默认颜色格式为 argb,暂不支持 alpha 通道 45 | preset_color_schemes: 46 | aqua: 47 | name: 碧水/Aqua 48 | author: 佛振 49 | color_format: abgr 50 | text_color: 0x000000 #默认文字颜色 51 | back_color: 0xeceeee #候选窗背景色 52 | shadow_color: 0x00000000 #候选窗阴影色,默认全透明(无阴影) 53 | border_color: 0xe0e0e0 #候选窗边框颜色 54 | hilited_text_color: 0x000000 #编码文字颜色 55 | hilited_back_color: 0xd4d4d4 #编码背景颜色 56 | hilited_shadow_color: 0x00000000 #编码背景块阴影颜色 57 | hilited_candidate_text_color: 0xffffff #高亮候选文字颜色 58 | hilited_candidate_back_color: 0xfa3a0a #高亮候选背景颜色 59 | hilited_candidate_shadow_color: 0x00000000 #高亮候选背景块阴影颜色 60 | candidate_text_color: 0x000000 #非高亮候选文字颜色 61 | candidate_back_color: 0xeceeee #非高亮候选背景颜色 62 | candidate_shadow_color: 0x00000000 #非高亮候选背景块阴影颜色 63 | 64 | azure: 65 | name: 青天/Azure 66 | author: 佛振 67 | color_format: abgr 68 | text_color: 0xffe8ca 69 | candidate_text_color: 0xfff8ee 70 | back_color: 0x8b4e01 71 | border_color: 0x8b4e01 72 | hilited_text_color: 0xfff8ee 73 | hilited_back_color: 0x8b4e01 74 | hilited_candidate_text_color: 0x7ffeff 75 | hilited_candidate_back_color: 0xa95e01 76 | comment_text_color: 0xc69664 77 | 78 | luna: 79 | name: 明月/Luna 80 | author: 佛振 81 | color_format: abgr 82 | text_color: 0x000000 83 | back_color: 0xffffff 84 | border_color: 0xcccccc 85 | hilited_text_color: 0x000000 86 | hilited_back_color: 0x7fffff 87 | hilited_candidate_text_color: 0xffffff 88 | hilited_candidate_back_color: 0x000000 89 | 90 | ink: 91 | name: 墨池/Ink 92 | author: 佛振 93 | color_format: abgr 94 | text_color: 0x000000 95 | back_color: 0xffffff 96 | border_color: 0x000000 97 | hilited_text_color: 0x000000 98 | hilited_back_color: 0xdddddd 99 | hilited_candidate_text_color: 0xffffff 100 | hilited_candidate_back_color: 0x000000 101 | 102 | lost_temple: 103 | name: 孤寺/Lost Temple 104 | author: 佛振 , based on ir_black 105 | color_format: abgr 106 | text_color: 0xe8f3f6 107 | back_color: 0x444444 108 | border_color: 0x444444 109 | hilited_text_color: 0x82e6ca 110 | hilited_back_color: 0x222222 111 | hilited_candidate_text_color: 0x000000 112 | hilited_candidate_back_color: 0x82e6ca 113 | 114 | dark_temple: 115 | name: 暗堂/Dark Temple 116 | author: 佛振 , based on ir_black 117 | color_format: abgr 118 | text_color: 0x92f6da 119 | candidate_text_color: 0xd8e3e6 120 | back_color: 0x222222 121 | border_color: 0x222222 122 | hilited_text_color: 0xffcf9a 123 | hilited_back_color: 0x222222 124 | hilited_candidate_text_color: 0x92f6da 125 | hilited_candidate_back_color: 0x333333 126 | comment_text_color: 0x606cff 127 | 128 | starcraft: 129 | name: 星際我爭霸/StarCraft 130 | author: Contralisk , original artwork by Blizzard Entertainment 131 | color_format: abgr 132 | text_color: 0xccaa88 133 | candidate_text_color: 0x30bb55 134 | back_color: 0x000000 135 | border_color: 0x1010a0 136 | hilited_text_color: 0xfecb96 137 | hilited_back_color: 0x000000 138 | hilited_candidate_text_color: 0x60ffa8 139 | hilited_candidate_back_color: 0x000000 140 | 141 | google: 142 | name: 谷歌/Google 143 | author: skoj 144 | color_format: abgr 145 | text_color: 0x666666 146 | candidate_text_color: 0x000000 147 | back_color: 0xFFFFFF 148 | border_color: 0xE2E2E2 149 | hilited_text_color: 0x000000 150 | hilited_back_color: 0xFFFFFF 151 | hilited_candidate_text_color: 0xFFFFFF 152 | hilited_candidate_back_color: 0xCE7539 153 | 154 | solarized_rock: 155 | name: 曬經石/Solarized Rock 156 | author: "Aben , based on Ethan Schoonover's Solarized color scheme" 157 | color_format: abgr 158 | back_color: 0x362b00 159 | border_color: 0x362b00 160 | text_color: 0x009985 161 | hilited_text_color: 0x98a12a 162 | candidate_text_color: 0x969483 163 | hilited_candidate_text_color: 0xffffff 164 | hilited_candidate_back_color: 0x8236d3 165 | 166 | tintin: 167 | name: "丁丁/ Tintin" 168 | author: "Patricivs " 169 | color_format: abgr 170 | text_color: 0xffffff 171 | back_color: 0xd99500 172 | border_color: 0xd99500 173 | label_color: 0xffffff 174 | candidate_text_color: 0xffffff 175 | comment_text_color: 0xffffff 176 | hilited_text_color: 0xf7d891 177 | hilited_back_color: 0xd99500 178 | hilited_candidate_text_color: 0xffffff 179 | hilited_comment_text_color: 0xffffff 180 | hilited_candidate_back_color: 0x2164f1 181 | 182 | dota_2: 183 | name: "DOTA 2" 184 | author: "Patricivs " 185 | color_format: abgr 186 | text_color: 0xffffff 187 | back_color: 0x120f10 188 | border_color: 0x120f10 189 | label_color: 0x5c758f 190 | hilited_text_color: 0x1841dd 191 | hilited_back_color: 0x120f10 192 | candidate_text_color: 0x5c758f 193 | comment_text_color: 0x5c758f 194 | hilited_candidate_text_color: 0xffffff 195 | hilited_comment_text_color: 0xffffff 196 | hilited_candidate_back_color: 0x1841dd 197 | 198 | brasil: 199 | name: "笆悉/Brasil" 200 | author: "Patricivs " 201 | color_format: abgr 202 | text_color: 0xffffff 203 | back_color: 0x559311 204 | border_color: 0x559311 205 | label_color: 0xffffff 206 | hilited_text_color: 0xffffff 207 | hilited_back_color: 0x7d3617 208 | candidate_text_color: 0xffffff 209 | comment_text_color: 0xffffff 210 | hilited_candidate_text_color: 0xffffff 211 | hilited_comment_text_color: 0xffffff 212 | hilited_candidate_back_color: 0x16c7f7 213 | 214 | doraemon: 215 | name: "銅鑼衛門/Doraemon" 216 | author: "Patricivs " 217 | color_format: abgr 218 | text_color: 0x1200e5 219 | back_color: 0xffffff 220 | border_color: 0xe89f00 221 | label_color: 0xe89f00 222 | hilited_text_color: 0xffffff 223 | hilited_back_color: 0x1200e5 224 | candidate_text_color: 0xe89f00 225 | comment_text_color: 0xe89f00 226 | hilited_candidate_text_color: 0xffffff 227 | hilited_comment_text_color: 0xffffff 228 | hilited_candidate_back_color: 0xe89f00 229 | 230 | espagna: 231 | name: "埃斯巴尼亞/España" 232 | author: "Patricivs " 233 | color_format: abgr 234 | text_color: 0xffffff 235 | back_color: 0x230dc3 236 | border_color: 0x230dc3 237 | label_color: 0xffffff 238 | hilited_text_color: 0xffffff 239 | hilited_back_color: 0x2cb5f7 240 | candidate_text_color: 0xffffff 241 | comment_text_color: 0xffffff 242 | hilited_candidate_text_color: 0xffffff 243 | hilited_comment_text_color: 0xffffff 244 | hilited_candidate_back_color: 0x2cb5f7 245 | 246 | gholabok: 247 | name: "胡蘿菔/Gholabok" 248 | author: "Patricivs " 249 | color_format: abgr 250 | text_color: 0xffffff 251 | back_color: 0x2939e8 252 | border_color: 0x2939e8 253 | label_color: 0xffffff 254 | hilited_text_color: 0xffffff 255 | hilited_back_color: 0x437b00 256 | candidate_text_color: 0xffffff 257 | comment_text_color: 0xffffff 258 | hilited_candidate_text_color: 0xffffff 259 | hilited_comment_text_color: 0xffffff 260 | hilited_candidate_back_color: 0x3d6ded 261 | 262 | kuma_shuzboz: 263 | name: "熊出沒/Kuma Shuzboz" 264 | author: "Patricivs " 265 | color_format: abgr 266 | text_color: 0x000000 267 | back_color: 0x2db6f8 268 | border_color: 0x2db6f8 269 | label_color: 0xffffff 270 | hilited_text_color: 0x2db6f8 271 | hilited_back_color: 0xffffff 272 | candidate_text_color: 0xffffff 273 | comment_text_color: 0xffffff 274 | hilited_candidate_text_color: 0xffffff 275 | hilited_comment_text_color: 0xffffff 276 | hilited_candidate_back_color: 0x000000 277 | 278 | kuon: 279 | name: "琨/Kuon" 280 | author: "Patricivs " 281 | color_format: abgr 282 | text_color: 0xffffff 283 | back_color: 0x70b33e 284 | border_color: 0x70b33e 285 | label_color: 0xffffff 286 | hilited_text_color: 0x70b33e 287 | hilited_back_color: 0xffffff 288 | candidate_text_color: 0xffffff 289 | comment_text_color: 0xffffff 290 | hilited_candidate_text_color: 0x70b33e 291 | hilited_comment_text_color: 0x70b33e 292 | hilited_candidate_back_color: 0xffffff 293 | 294 | macau: 295 | name: "澳門/Macau" 296 | author: "Patricivs " 297 | color_format: abgr 298 | text_color: 0x00d9ff 299 | back_color: 0x81a300 300 | border_color: 0x81a300 301 | label_color: 0xffffff 302 | hilited_text_color: 0xffffff 303 | hilited_back_color: 0x00d9ff 304 | candidate_text_color: 0xffffff 305 | comment_text_color: 0xffffff 306 | hilited_candidate_text_color: 0x00d9ff 307 | hilited_comment_text_color: 0x00d9ff 308 | hilited_candidate_back_color: 0xffffff 309 | 310 | nba: 311 | name: "NBA" 312 | author: "Patricivs " 313 | color_format: abgr 314 | text_color: 0xffffff 315 | back_color: 0xb76a00 316 | border_color: 0xb76a00 317 | label_color: 0xffffff 318 | hilited_text_color: 0x541ed7 319 | hilited_back_color: 0xffffff 320 | candidate_text_color: 0xffffff 321 | comment_text_color: 0xffffff 322 | hilited_candidate_text_color: 0xffffff 323 | hilited_comment_text_color: 0xffffff 324 | hilited_candidate_back_color: 0x541ed7 325 | 326 | ps4: 327 | name: "遊驛四/PS4" 328 | author: "Patricivs " 329 | color_format: abgr 330 | text_color: 0xffffff 331 | back_color: 0x000000 332 | border_color: 0x000000 333 | label_color: 0xffffff 334 | hilited_text_color: 0xffffff 335 | hilited_back_color: 0x575759 336 | candidate_text_color: 0xffffff 337 | comment_text_color: 0xffffff 338 | hilited_candidate_text_color: 0xffffff 339 | hilited_comment_text_color: 0xffffff 340 | hilited_candidate_back_color: 0xe89f00 341 | 342 | skype: 343 | name: "斯蓋普/Skype" 344 | author: "Patricivs " 345 | color_format: abgr 346 | text_color: 0xffffff 347 | back_color: 0xefad00 348 | border_color: 0xefad00 349 | label_color: 0xffffff 350 | hilited_text_color: 0xefad00 351 | hilited_back_color: 0xffffff 352 | candidate_text_color: 0xffffff 353 | comment_text_color: 0xffffff 354 | hilited_candidate_text_color: 0xefad00 355 | hilited_comment_text_color: 0xefad00 356 | hilited_candidate_back_color: 0xffffff 357 | 358 | xbox_silver: 359 | name: "銀色叉盒/Xbox Silver" 360 | author: "Patricivs " 361 | color_format: abgr 362 | text_color: 0x1fc28d 363 | back_color: 0xefeeee 364 | border_color: 0xefeeee 365 | label_color: 0x5bf0b5 366 | hilited_text_color: 0xffffff 367 | hilited_back_color: 0x5bf0b5 368 | candidate_text_color: 0x1fc28d 369 | comment_text_color: 0x1fc28d 370 | hilited_candidate_text_color: 0xffffff 371 | hilited_comment_text_color: 0xffffff 372 | hilited_candidate_back_color: 0x448c28 373 | 374 | youtube: 375 | name: "YouTube" 376 | author: "Patricivs " 377 | color_format: abgr 378 | text_color: 0x000000 379 | back_color: 0xdedede 380 | border_color: 0xdedede 381 | label_color: 0x000000 382 | hilited_text_color: 0x230dc3 383 | hilited_back_color: 0xffffff 384 | candidate_text_color: 0x000000 385 | comment_text_color: 0x000000 386 | hilited_candidate_text_color: 0xffffff 387 | hilited_comment_text_color: 0xffffff 388 | hilited_candidate_back_color: 0x230dc3 389 | 390 | so_young: 391 | name: "致青春/So Young" 392 | author: "五磅兔 " 393 | color_format: abgr 394 | text_color: 0x8236d3 395 | back_color: 0xe3f6fd 396 | border_color: 0xd5e8ee 397 | label_color: 0xa1a193 398 | candidate_text_color: 0x837b65 399 | comment_text_color: 0xd28b26 400 | hilited_text_color: 0x969483 401 | hilited_back_color: 0xd5e8ee 402 | hilited_candidate_text_color: 0xd5e8ee 403 | hilited_comment_text_color: 0xd5e8ee 404 | hilited_candidate_back_color: 0x98a12a 405 | 406 | smurfs: 407 | name: "藍精靈/Smurfs" 408 | author: "skoj " 409 | color_format: abgr 410 | text_color: 0xffffff 411 | back_color: 0xbf7817 412 | border_color: 0xf5ede0 413 | label_color: 0xbf7817 414 | hilited_text_color: 0xdbbc6d 415 | hilited_back_color: 0xbf7817 416 | candidate_text_color: 0xf6f6f6 417 | comment_text_color: 0xf6f6f6 418 | hilited_candidate_text_color: 0xf6f6f6 419 | hilited_comment_text_color: 0xf6f6f6 420 | hilited_candidate_back_color: 0xdbbc6d 421 | 422 | wii: 423 | name: "Wii" 424 | author: "Patricivs " 425 | color_format: abgr 426 | text_color: 0x575759 427 | back_color: 0xefefef 428 | border_color: 0xefefef 429 | label_color: 0xcac9c8 430 | hilited_text_color: 0xffcc33 431 | hilited_back_color: 0xefefef 432 | candidate_text_color: 0x575759 433 | comment_text_color: 0xcac9c8 434 | hilited_candidate_text_color: 0xffffff 435 | hilited_comment_text_color: 0xffffff 436 | hilited_candidate_back_color: 0xffcc33 437 | 438 | android: 439 | name: "安卓/Android" 440 | author: "Patricivs " 441 | color_format: abgr 442 | text_color: 0xffffff 443 | back_color: 0x99731c 444 | border_color: 0x99731c 445 | label_color: 0xc18835 446 | hilited_text_color: 0x50c4a8 447 | hilited_back_color: 0x99731c 448 | candidate_text_color: 0xffffff 449 | comment_text_color: 0xffffff 450 | hilited_candidate_text_color: 0xffffff 451 | hilited_comment_text_color: 0xffffff 452 | hilited_candidate_back_color: 0x50c4a8 453 | 454 | cool_breeze: 455 | name: "清風/Cool Breeze" 456 | author: "skoj " 457 | color_format: abgr 458 | text_color: 0x0000FF 459 | back_color: 0xFFFBFB 460 | border_color: 0xFFAAAA 461 | hilited_text_color: 0x0000CE 462 | hilited_back_color: 0xFFFBFB 463 | candidate_text_color: 0x009100 464 | hilited_candidate_text_color: 0x6F003A 465 | hilited_candidate_back_color: 0xFFD6AC 466 | 467 | google_plus: 468 | name: "Google+" 469 | author: "Patricivs " 470 | color_format: abgr 471 | text_color: 0xcac9c8 472 | back_color: 0xffffff 473 | border_color: 0x394bdd 474 | label_color: 0xcac9c8 475 | hilited_text_color: 0x394bdd 476 | hilited_back_color: 0xffffff 477 | candidate_text_color: 0x394bdd 478 | comment_text_color: 0xcac9c8 479 | hilited_candidate_text_color: 0xffffff 480 | hilited_comment_text_color: 0xffffff 481 | hilited_candidate_back_color: 0x394bdd 482 | 483 | modern_warfare: 484 | name: "現代戰爭/Modern Warfare" 485 | author: P1461 486 | color_format: abgr 487 | text_color: 0x14bc70 488 | back_color: 0x0a1b0d 489 | border_color: 0x4bad83 490 | hilited_text_color: 0xfbfdfc 491 | hilited_back_color: 0x030e06 492 | candidate_text_color: 0xabfedc 493 | comment_text_color: 0xfcfdfb 494 | hilited_candidate_text_color: 0xabfedc 495 | hilited_candidate_back_color: 0x676f63 496 | 497 | brisk: 498 | name: "輕盈/Brisk" 499 | author: "skoj " 500 | color_format: abgr 501 | text_color: 0x2238dc 502 | back_color: 0xffffff 503 | border_color: 0x333333 504 | hilited_text_color: 0x2238dc 505 | hilited_back_color: 0xffffff 506 | candidate_text_color: 0x575757 507 | hilited_candidate_text_color: 0x2238dc 508 | hilited_candidate_back_color: 0xffffff 509 | 510 | starcraft_ii: 511 | name: "星際爭霸Ⅱ/StarCraft Ⅱ" 512 | author: "Patricivs " 513 | color_format: abgr 514 | text_color: 0xffffff 515 | back_color: 0x29190a 516 | border_color: 0x534b46 517 | label_color: 0xffffff 518 | hilited_text_color: 0xffffff 519 | hilited_back_color: 0x17100a 520 | candidate_text_color: 0xffffff 521 | comment_text_color: 0xffffff 522 | hilited_candidate_text_color: 0xffffff 523 | hilited_comment_text_color: 0xffffff 524 | hilited_candidate_back_color: 0xefad1e 525 | 526 | steam: 527 | name: "Steam" 528 | author: "Patricivs " 529 | color_format: abgr 530 | text_color: 0xcd8c52 531 | back_color: 0x141617 532 | border_color: 0x353638 533 | label_color: 0xffffff 534 | hilited_text_color: 0xc9cfd1 535 | hilited_back_color: 0x141617 536 | candidate_text_color: 0xffffff 537 | comment_text_color: 0xa7a7a9 538 | hilited_candidate_text_color: 0xffffff 539 | hilited_comment_text_color: 0xa7a7a9 540 | hilited_candidate_back_color: 0x594231 541 | 542 | flypy: 543 | # description: | 544 | # 小鹤飞扬:白底蓝字,红色高亮。 545 | # 根据小鹤双拼官网图片制作 546 | # http://www.flypy.com/images/mr.png 547 | name: "小鹤飞扬/flypy" 548 | author: "Pal.lxk " 549 | color_format: abgr 550 | text_color: 0x000000 551 | back_color: 0xffffff 552 | border_color: 0xc6c6c6 553 | label_color: 0xff8000 554 | hilited_text_color: 0xff8000 555 | hilited_back_color: 0xffffff 556 | candidate_text_color: 0xff8000 557 | comment_text_color: 0xff8000 558 | hilited_candidate_text_color: 0x0000b0 559 | hilited_comment_text_color: 0x0000b0 560 | hilited_candidate_back_color: 0xffffff 561 | 562 | metroblue: 563 | name: "现代蓝/Metro Blue" 564 | author: "Prcuvu " 565 | color_format: abgr 566 | text_color: 0x000000 567 | back_color: 0xffffff 568 | border_color: 0xd77800 569 | label_color: 0x838383 570 | hilited_text_color: 0x000000 571 | hilited_back_color: 0xffffff 572 | candidate_text_color: 0x000000 573 | comment_text_color: 0x000000 574 | hilited_candidate_text_color: 0xffffff 575 | hilited_comment_text_color: 0xffffff 576 | hilited_candidate_back_color: 0xd77800 577 | hilited_label_color: 0xffffff 578 | 579 | psionics: 580 | name: 幽能/Psionics 581 | author: 雨過之後、佛振 582 | color_format: abgr 583 | text_color: 0xc2c2c2 584 | back_color: 0x444444 585 | border_color: 0x444444 586 | candidate_text_color: 0xeeeeee 587 | hilited_text_color: 0xeeeeee 588 | hilited_back_color: 0x444444 589 | hilited_candidate_label_color: 0xfafafa 590 | hilited_candidate_text_color: 0xfafafa 591 | hilited_candidate_back_color: 0xd8bf00 592 | comment_text_color: 0x808080 593 | hilited_comment_text_color: 0x444444 594 | -------------------------------------------------------------------------------- /Lib/GetCaretPosEx/GetCaretPosEx.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | @example 3 | f1:: { 4 | if GetCaretPosEx(&left, &top, &right, &bottom, true) { 5 | A_CoordModeToolTip := "Screen" 6 | ToolTip "Hello", left, bottom 7 | } 8 | } 9 | */ 10 | GetCaretPosEx(&left?, &top?, &right?, &bottom?, useHook := false) { 11 | if getCaretPosFromGui(&hwnd := 0) 12 | return true 13 | try 14 | className := WinGetClass(hwnd) 15 | catch 16 | className := "" 17 | if className ~= "^(?:Windows|Microsoft)\.UI\..+" 18 | funcs := [getCaretPosFromUIA, getCaretPosFromHook, getCaretPosFromMSAA] 19 | else if className ~= "^HwndWrapper\[PowerShell_ISE\.exe;;[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\]" 20 | funcs := [getCaretPosFromHook, getCaretPosFromWpfCaret] 21 | else 22 | funcs := [getCaretPosFromMSAA, getCaretPosFromUIA, getCaretPosFromHook] 23 | for fn in funcs { 24 | if fn == getCaretPosFromHook && !useHook 25 | continue 26 | if fn() 27 | return true 28 | } 29 | return false 30 | 31 | getCaretPosFromGui(&hwnd) { 32 | x64 := A_PtrSize == 8 33 | guiThreadInfo := Buffer(x64 ? 72 : 48) 34 | NumPut("uint", guiThreadInfo.Size, guiThreadInfo) 35 | if DllCall("GetGUIThreadInfo", "uint", 0, "ptr", guiThreadInfo) { 36 | if hwnd := NumGet(guiThreadInfo, x64 ? 48 : 28, "ptr") { 37 | getRect(guiThreadInfo.Ptr + (x64 ? 56 : 32), &left, &top, &right, &bottom) 38 | scaleRect(getWindowScale(hwnd), &left, &top, &right, &bottom) 39 | clientToScreenRect(hwnd, &left, &top, &right, &bottom) 40 | return true 41 | } 42 | hwnd := NumGet(guiThreadInfo, x64 ? 16 : 12, "ptr") 43 | } 44 | return false 45 | } 46 | 47 | getCaretPosFromMSAA() { 48 | if !hOleacc := DllCall("LoadLibraryW", "str", "oleacc.dll", "ptr") 49 | return false 50 | hOleacc := { Ptr: hOleacc, __Delete: (_) => DllCall("FreeLibrary", "ptr", _) } 51 | static IID_IAccessible := guidFromString("{618736e0-3c3d-11cf-810c-00aa00389b71}") 52 | if !DllCall("oleacc\AccessibleObjectFromWindow", "ptr", hwnd, "uint", 0xfffffff8, "ptr", IID_IAccessible, "ptr*", accCaret := ComValue(13, 0), "int") { 53 | if A_PtrSize == 8 { 54 | varChild := Buffer(24, 0) 55 | NumPut("ushort", 3, varChild) 56 | hr := ComCall(22, accCaret, "int*", &x := 0, "int*", &y := 0, "int*", &w := 0, "int*", &h := 0, "ptr", varChild, "int") 57 | } 58 | else { 59 | hr := ComCall(22, accCaret, "int*", &x := 0, "int*", &y := 0, "int*", &w := 0, "int*", &h := 0, "int64", 3, "int64", 0, "int") 60 | } 61 | if !hr { 62 | pt := x | y << 32 63 | DllCall("ScreenToClient", "ptr", hwnd, "int64*", &pt) 64 | left := pt & 0xffffffff 65 | top := pt >> 32 66 | right := left + w 67 | bottom := top + h 68 | scaleRect(getWindowScale(hwnd), &left, &top, &right, &bottom) 69 | clientToScreenRect(hwnd, &left, &top, &right, &bottom) 70 | return true 71 | } 72 | } 73 | return false 74 | } 75 | 76 | getCaretPosFromUIA() { 77 | try { 78 | uia := ComObject("{E22AD333-B25F-460C-83D0-0581107395C9}", "{30CBE57D-D9D0-452A-AB13-7AC5AC4825EE}") 79 | ComCall(20, uia, "ptr*", cacheRequest := ComValue(13, 0)) ; uia->CreateCacheRequest(&cacheRequest); 80 | if !cacheRequest.Ptr 81 | return false 82 | ComCall(4, cacheRequest, "ptr", 10014) ; cacheRequest->AddPattern(UIA_TextPatternId); 83 | ComCall(4, cacheRequest, "ptr", 10024) ; cacheRequest->AddPattern(UIA_TextPattern2Id); 84 | 85 | ComCall(12, uia, "ptr", cacheRequest, "ptr*", focusedEle := ComValue(13, 0)) ; uia->GetFocusedElementBuildCache(cacheRequest, &focusedEle); 86 | if !focusedEle.Ptr 87 | return false 88 | 89 | static IID_IUIAutomationTextPattern2 := guidFromString("{506a921a-fcc9-409f-b23b-37eb74106872}") 90 | range := ComValue(13, 0) 91 | ComCall(15, focusedEle, "int", 10024, "ptr", IID_IUIAutomationTextPattern2, "ptr*", textPattern := ComValue(13, 0)) ; focusedEle->GetCachedPatternAs(UIA_TextPattern2Id, IID_PPV_ARGS(&textPattern)); 92 | if textPattern.Ptr { 93 | ComCall(10, textPattern, "int*", &isActive := 0, "ptr*", range) ; textPattern->GetCaretRange(&isActive, &range); 94 | if range.Ptr 95 | goto getRangeInfo 96 | } 97 | ; If no caret range, get selection range. 98 | static IID_IUIAutomationTextPattern := guidFromString("{32eba289-3583-42c9-9c59-3b6d9a1e9b6a}") 99 | ComCall(15, focusedEle, "int", 10014, "ptr", IID_IUIAutomationTextPattern, "ptr*", textPattern) ; focusedEle->GetCachedPatternAs(UIA_TextPatternId, IID_PPV_ARGS(&textPattern)); 100 | if textPattern.Ptr { 101 | ComCall(5, textPattern, "ptr*", ranges := ComValue(13, 0)) ; textPattern->GetSelection(&ranges); 102 | if ranges.Ptr { 103 | ; Retrieve the last selection range. 104 | ComCall(3, ranges, "int*", &len := 0) ; ranges->get_Length(&len); 105 | if len > 0 { 106 | ComCall(4, ranges, "int", len - 1, "ptr*", range) ; ranges->GetElement(len - 1, &range); 107 | if range.Ptr { 108 | ; Collapse the range. 109 | ComCall(15, range, "int", 0, "ptr", range, "int", 1) ; range->MoveEndpointByRange(TextPatternRangeEndpoint_Start, range, TextPatternRangeEndpoint_End); 110 | goto getRangeInfo 111 | } 112 | } 113 | } 114 | } 115 | return false 116 | getRangeInfo: 117 | psa := 0 118 | ; This is a degenerate text range, we have to expand it. 119 | ComCall(6, range, "int", 0) ; range->ExpandToEnclosingUnit(TextUnit_Character); 120 | ComCall(10, range, "ptr*", &psa) ; range->GetBoundingRectangles(&psa); 121 | if psa { 122 | rects := ComValue(0x2005, psa, 1) ; SafeArray 123 | if rects.MaxIndex() >= 3 { 124 | rects[2] := 0 125 | goto end 126 | } 127 | } 128 | ; ExpandToEnclosingUnit by character may be invalid in some control if the range is at the end of the document. 129 | ; Assume that the range is at the end of the document and not in an empty line, try to expand it by line. 130 | ComCall(6, range, "int", 3) ; range->ExpandToEnclosingUnit(TextUnit_Line) 131 | ComCall(10, range, "ptr*", &psa) ; range->GetBoundingRectangles(&psa); 132 | if psa { 133 | rects := ComValue(0x2005, psa, 1) ; SafeArray 134 | if rects.MaxIndex() >= 3 { 135 | ; Here rects is {x, y, w, h}, we take the end endpoint as the caret position. 136 | rects[0] := rects[0] + rects[2] 137 | rects[2] := 0 138 | goto end 139 | } 140 | } 141 | return false 142 | end: 143 | left := Round(rects[0]) 144 | top := Round(rects[1]) 145 | right := left + Round(rects[2]) 146 | bottom := top + Round(rects[3]) 147 | return true 148 | } 149 | return false 150 | } 151 | 152 | getCaretPosFromWpfCaret() { 153 | try { 154 | uia := ComObject("{E22AD333-B25F-460C-83D0-0581107395C9}", "{30CBE57D-D9D0-452A-AB13-7AC5AC4825EE}") 155 | ComCall(8, uia, "ptr*", focusedEle := ComValue(13, 0)) ; uia->GetFocusedElement(&focusedEle); 156 | if !focusedEle.Ptr 157 | return false 158 | 159 | ComCall(20, uia, "ptr*", cacheRequest := ComValue(13, 0)) ; uia->CreateCacheRequest(&cacheRequest); 160 | if !cacheRequest.Ptr 161 | return false 162 | 163 | ComCall(17, uia, "ptr*", rawViewCondition := ComValue(13, 0)) ; uia->get_RawViewCondition(&rawViewCondition); 164 | if !rawViewCondition.Ptr 165 | return false 166 | 167 | ComCall(9, cacheRequest, "ptr", rawViewCondition) ; cacheRequest->put_TreeFilter(rawViewCondition); 168 | ComCall(3, cacheRequest, "int", 30001) ; cacheRequest->AddProperty(UIA_BoundingRectanglePropertyId); 169 | 170 | var := Buffer(24, 0) 171 | ref := ComValue(0x400C, var.Ptr) 172 | ref[] := ComValue(8, "WpfCaret") 173 | ComCall(23, uia, "int", 30012, "ptr", var, "ptr*", condition := ComValue(13, 0)) ; uia->CreatePropertyCondition(UIA_ClassNamePropertyId, CComVariant(L"WpfCaret"), &classNameCondition); 174 | if !condition.Ptr 175 | return false 176 | 177 | ComCall(7, focusedEle, "int", 4, "ptr", condition, "ptr", cacheRequest, "ptr*", wpfCaret := ComValue(13, 0)) ; focusedEle->FindFirstBuildCache(TreeScope_Descendants, condition, cacheRequest, &wpfCaret); 178 | if !wpfCaret.Ptr 179 | return false 180 | 181 | ComCall(75, wpfCaret, "ptr", rect := Buffer(16)) ; wpfCaret->get_CachedBoundingRectangle(&rect); 182 | getRect(rect, &left, &top, &right, &bottom) 183 | return true 184 | } 185 | return false 186 | } 187 | 188 | getCaretPosFromHook() { 189 | static WM_GET_CARET_POS := DllCall("RegisterWindowMessageW", "str", "WM_GET_CARET_POS", "uint") 190 | if !tid := DllCall("GetWindowThreadProcessId", "ptr", hwnd, "ptr*", &pid := 0, "uint") 191 | return false 192 | ; Update caret position 193 | try { 194 | SendMessage(0x010f, 0, 0, hwnd) ; WM_IME_COMPOSITION 195 | } 196 | ; PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ 197 | if !hProcess := DllCall("OpenProcess", "uint", 1082, "int", false, "uint", pid, "ptr") 198 | return false 199 | hProcess := { Ptr: hProcess, __Delete: (_) => DllCall("CloseHandle", "ptr", _) } 200 | 201 | isX64 := isX64Process(hProcess) 202 | if isX64 && A_PtrSize == 4 203 | return false 204 | if !moduleBaseMap := getModulesBases(hProcess, ["kernel32.dll", "user32.dll", "combase.dll"]) 205 | return false 206 | if isX64 { 207 | static shellcode64 := compile(true) 208 | shellcode := shellcode64 209 | } 210 | else { 211 | static shellcode32 := compile(false) 212 | shellcode := shellcode32 213 | } 214 | if !mem := DllCall("VirtualAllocEx", "ptr", hProcess, "ptr", 0, "ptr", shellcode.Size, "uint", 0x1000, "uint", 0x40, "ptr") 215 | return false 216 | mem := { Ptr: mem, __Delete: (_) => DllCall("VirtualFreeEx", "ptr", hProcess, "ptr", _, "uptr", 0, "uint", 0x8000) } 217 | link(isX64, shellcode, mem.Ptr, moduleBaseMap["user32.dll"], moduleBaseMap["combase.dll"], hwnd, tid, WM_GET_CARET_POS, &pThreadProc, &pRect) 218 | 219 | if !DllCall("WriteProcessMemory", "ptr", hProcess, "ptr", mem, "ptr", shellcode, "uptr", shellcode.Size, "ptr", 0) 220 | return false 221 | DllCall("FlushInstructionCache", "ptr", hProcess, "ptr", mem, "uptr", shellcode.Size) 222 | 223 | if !hThread := DllCall("CreateRemoteThread", "ptr", hProcess, "ptr", 0, "uptr", 0, "ptr", pThreadProc, "ptr", mem, "uint", 0, "uint*", &remoteTid := 0, "ptr") 224 | return false 225 | hThread := { Ptr: hThread, __Delete: (_) => DllCall("CloseHandle", "ptr", _) } 226 | 227 | if msgWaitForSingleObject(hThread) 228 | return false 229 | if !DllCall("GetExitCodeThread", "ptr", hThread, "uint*", exitCode := 0) || exitCode !== 0 230 | return false 231 | 232 | rect := Buffer(16) 233 | if !DllCall("ReadProcessMemory", "ptr", hProcess, "ptr", pRect, "ptr", rect, "uptr", rect.Size, "uptr*", &bytesRead := 0) || bytesRead !== rect.Size 234 | return false 235 | getRect(rect, &left, &top, &right, &bottom) 236 | scaleRect(getWindowScale(hwnd), &left, &top, &right, &bottom) 237 | return true 238 | 239 | static isX64Process(hProcess) { 240 | DllCall("IsWow64Process", "ptr", hProcess, "int*", &isWow64 := 0) 241 | if isWow64 242 | return false 243 | if A_PtrSize == 8 244 | return true 245 | DllCall("IsWow64Process", "ptr", DllCall("GetCurrentProcess", "ptr"), "int*", &isWow64) 246 | return isWow64 247 | } 248 | 249 | static getModulesBases(hProcess, modules) { 250 | hModules := Buffer(A_PtrSize * 350) 251 | if !DllCall("K32EnumProcessModulesEx", "ptr", hProcess, "ptr", hModules, "uint", hModules.Size, "uint*", &needed := 0, "uint", 3) 252 | return 253 | moduleBaseMap := Map() 254 | moduleBaseMap.CaseSense := false 255 | for v in modules 256 | moduleBaseMap[v] := 0 257 | cnt := modules.Length 258 | loop Min(350, needed) { 259 | hModule := NumGet(hModules, A_PtrSize * (A_Index - 1), "ptr") 260 | VarSetStrCapacity(&name, 12) 261 | if DllCall("K32GetModuleBaseNameW", "ptr", hProcess, "ptr", hModule, "str", &name, "uint", 13) { 262 | if moduleBaseMap.Has(name) { 263 | moduleInfo := Buffer(24) 264 | if !DllCall("K32GetModuleInformation", "ptr", hProcess, "ptr", hModule, "ptr", moduleInfo, "uint", moduleInfo.Size) 265 | return 266 | if !base := NumGet(moduleInfo, "ptr") 267 | return 268 | moduleBaseMap[name] := base 269 | cnt-- 270 | } 271 | } 272 | } until cnt == 0 273 | if cnt == 0 274 | return moduleBaseMap 275 | } 276 | 277 | static compile(x64) { 278 | if x64 279 | shellcodeBase64 := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABrnppSh2UjT6uenH1oPjxQAeiAqiEg0hGT4ABgsGe4blNldFdpbmRvd3NIb29rRXhXAAAAVW5ob29rV2luZG93c0hvb2tFeABDYWxsTmV4dEhvb2tFeAAAAAAAAFNlbmRNZXNzYWdlVGltZW91dFcAQ29DcmVhdGVJbnN0YW5jZQAAAAAAAAAASIlcJAhIiXQkEFdIg+wgSYvYSIvyi/mFyXgjSIXbdB6LBQb///9BOUAQdRJIjQ3d/v//6JgBAACJBfL+//9Iiw3L/v//SI0VdP///+jnAgAASIXAdRBIi1wkMEiLdCQ4SIPEIF/DTIvLTIvGi9czyUiLXCQwSIt0JDhIg8QgX0j/4MzMzMzMzDPAw8zMzMzMQFNWSIPsSIvySIvZSIXJdQy4VwAHgEiDxEheW8NIi0kISI1UJGBIiVQkKEG4/////0iNVCQwSIl8JEAz/0iJVCQgiXwkYIvWSIsBRI1PAf9QKIXAeHJIOXwkMHRrOXwkYHRlSItLCEiNVCR4SIl8JHhIiwH/UEiL+IXAeDJIi0wkeEiFyXQoSIsBSI1UJHBMi0QkMEyNSxBIiVQkIIvW/1AgSItMJHiL+EiLAf9QEEiLTCQwSIsB/1AQi8dIi3wkQEiDxEheW8NIi3wkQLgBAAAASIPESF5bw8zMzMzMzMxIhcl0VEiF0nRPTYXAdEpIiwJIhcB1HUi4wAAAAAAAAEZIOUIIdCxJxwAAAAAAuAJAAIDDSbkD6ICqISDSEUk7wXXkSLiT4ABgsGe4bkg5Qgh11EmJCDPAw7hXAAeAw8xAU0iD7EBIi9lIjZHYAAAASItJCOhPAQAASIXAdQu4AQAAAEiDxEBbwzPJx0QkWAEAAABIjVQkaEiJTCRoSIlUJCBMjUt4M9JIiUwkYEiJTCQwiUwkUEiNS2hEjUIX/9CFwA+I7wAAAEiLTCRoSIXJD4ThAAAASIsBSI1UJFD/UBiFwA+IhQAAAEiLTCRoSI1UJGBIiwH/UDiFwHhxSItMJGBIhcl0bEiLAUiNVCQw/1AwhcB4WEiLTCQwSIXJdGZIjUNISIlLMEiJQyhMjUMoSI0Vyf7//0G5AwAAAEiJEEiNBdH9//9IiUNQSI1UJFhIiUNYSI0Fxf3//0iJQ2BIiwFIiVQkIItUJFD/UBhIi0wkYEiLVCQwSIXSdA5IiwJIi8r/UBBIi0wkYEiFyXQGSIsB/1AQSItMJGhIhcl0BkiLAf9QEItEJFj32BvAg+AESIPEQFvDuAQAAABIg8RAW8PMzMzMzMxIiVwkCEiJbCQQSIl0JBhIiXwkIEyL2kyL0UiFyXRwSIXSdGtIY0E8g7wIjAAAAAB0XYuMCIgAAACFyXRSRYtMCiBJjQQKi3AkTQPKi2gcSQPyi3gYSQPqD7YaRTPA/89BixFJA9I6GnUZD7bLSYvDSSvThMl0Lw+2SAFI/8A6DAJ08EH/wEmDwQREO8d20TPASItcJAhIi2wkEEiLdCQYSIt8JCDDSWPAD7cMRotEjQBJA8Lr28zMSIlcJAhIiWwkEEiJdCQYSIl8JCBBVkiD7EBIixlIjZGIAAAASIv5SIvL6Bn///9IjZfEAAAASIvLSIvw6Af///9IjZecAAAASIvLSIvo6PX+//9Mi/BIhfZ0ZUiF7XRgSIXAdFtEi08YSI0VoPv//0UzwEGNSAT/1kiL8EiFwHUFjUYC6z+LVxwzwEiLTxBFM8lIiUQkMEUzwMdEJCjIAAAAiUQkIP/VSIvOSIvYQf/WSIXbdQWNQwPrCotHIOsFuAEAAABIi1wkUEiLbCRYSIt0JGBIi3wkaEiDxEBBXsM=" 280 | else 281 | shellcodeBase64 := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGuemlKHZSNPq56cfWg+PFAB6ICqISDSEZPgAGCwZ7huU2V0V2luZG93c0hvb2tFeFcAAABVbmhvb2tXaW5kb3dzSG9va0V4AENhbGxOZXh0SG9va0V4AAAAAAAAU2VuZE1lc3NhZ2VUaW1lb3V0VwBDb0NyZWF0ZUluc3RhbmNlAAAAAFZX6MkCAACDfCQMAIvwi3wkFHwYhf90FItPCDtOEHUMVuhqAQAAg8QEiUYUjYaIAAAAUP826J4CAACDxAiFwHUFX17CDABX/3QkFP90JBRqAP/QX17CDAAzwMIEAMzMzIPsFFaLdCQchfZ1DLhXAAeAXoPEFMIIAItOBI1UJARSjVQkEMdEJAgAAAAAUosBagFq//90JDBR/1AUhcB4bIN8JAwAdGWDfCQEAHRei04EjVQkHFfHRCQgAAAAAFKLAVH/UCSL+IX/eC2LVCQghdJ0JYsCi0gQjUQkDFCNRghQ/3QkGP90JDBS/9GL+ItEJCBQiwj/UQiLRCQQUIsI/1EIi8dfXoPEFMIIALgBAAAAXoPEFMIIAMyLTCQIVot0JAiF9nRfhcl0W4tUJBCF0nRTiwELQQR1IYF5CMAAAAB1CYF5DAAAAEZ0MscCAAAAALgCQACAXsIMAIE5A+iAqnXpgXkEISDSEXXggXkIk+AAYHXXgXkMsGe4bnXOiTIzwF7CDAC4VwAHgF7CDADMzMyD7BBWi3QkGI2GsAAAAFD/dgToMQEAAIvIg8QIhcl1CI1BAV6DxBDDjUQkBMdEJAQAAAAAUI1GUMdEJBwAAAAAUGoXagCNRkDHRCQYAAAAAFDHRCQgAAAAAMdEJCQBAAAA/9GFwA+IywAAAItMJASFyQ+EvwAAAIsBjVQkDFdSUf9QDIXAeHCLTCQIjVQkHFJRiwH/UByFwHhdi0wkHIXJdFmLAY1UJAxSUf9QGIXAeEaLfCQMhf90UI1OMIl+HLjcAQAAiU4YA8aNVhiJAYvGBRwBAACNTCQUUYlGNIlGOLgkAQAAagMDxlL/dCQciUY8iwdX/1AMi0wkHItUJAyF0nQKiwJS/1AIi0wkHF+FyXQGiwFR/1AIi0wkBIXJdAaLAVH/UAiLRCQQ99heG8CD4ASDxBDDuAQAAABeg8QQw7gAAAAAw8zMg+wIU1VWV4t8JByF/w+EgQAAAItcJCCF23R5i0c8g3w4fAB0b4tEOHiFwHRni0w4JDP2i1Q4IAPPi2w4GAPXiUwkEItMOBwDz4lUJByJTCQUTYorixSyA9c6KnUTis2LwyvThMl0FIpIAUA6DAJ080Y79Xcfi1QkHOvZi0QkEItMJBQPtwRwiwSBA8dfXl1bg8QIw19eXTPAW4PECMPMzFNVVleLfCQUizeNR2BQVuhM////iUQkHI2HnAAAAFBW6Dv///+L2I1HdFBW6C////+LTCQsg8QYi+iFyXRshdt0aIXtdGSLxwWUAwAAiXgBuMQAAAD/dwwDx2oAUGoE/9GJRCQUhcB1DF9eXbgCAAAAW8IEAGoAaMgAAABqAGoAagD/dxD/dwj/0/90JBSL8P/VhfZ1Cl+NRgNeXVvCBACLRxRfXl1bwgQAX15duAEAAABbwgQA" 282 | len := StrLen(shellcodeBase64) 283 | shellcode := Buffer(len * 0.75) 284 | if !DllCall("crypt32\CryptStringToBinary", "str", shellcodeBase64, "uint", len, "uint", 1, "ptr", shellcode, "uint*", shellcode.Size, "ptr", 0, "ptr", 0) 285 | return 286 | return shellcode 287 | } 288 | 289 | static link(x64, shellcode, shellcodeBase, user32Base, combaseBase, hwnd, tid, msg, &pThreadProc, &pRect) { 290 | if x64 { 291 | NumPut("uint64", user32Base, shellcode, 0) 292 | NumPut("uint64", combaseBase, shellcode, 8) 293 | NumPut("uint64", hwnd, shellcode, 16) 294 | NumPut("uint", tid, shellcode, 24) 295 | NumPut("uint", msg, shellcode, 28) 296 | pThreadProc := shellcodeBase + 0x4e0 297 | pRect := shellcodeBase + 56 298 | } 299 | else { 300 | NumPut("uint", user32Base, shellcode, 0) 301 | NumPut("uint", combaseBase, shellcode, 4) 302 | NumPut("uint", hwnd, shellcode, 8) 303 | NumPut("uint", tid, shellcode, 12) 304 | NumPut("uint", msg, shellcode, 16) 305 | pThreadProc := shellcodeBase + 0x43c 306 | pRect := shellcodeBase + 32 307 | } 308 | } 309 | 310 | static msgWaitForSingleObject(handle) { 311 | while 1 == res := DllCall("MsgWaitForMultipleObjects", "uint", 1, "ptr*", handle, "int", false, "uint", -1, "uint", 7423) { ; QS_ALLINPUT := 7423 312 | msg := Buffer(A_PtrSize == 8 ? 48 : 28) 313 | while DllCall("PeekMessageW", "ptr", msg, "ptr", 0, "uint", 0, "uint", 0, "uint", 1) { ; PM_REMOVE := 1 314 | DllCall("TranslateMessage", "ptr", msg) 315 | DllCall("DispatchMessageW", "ptr", msg) 316 | } 317 | } 318 | return res 319 | } 320 | } 321 | 322 | static guidFromString(str) { 323 | DllCall("ole32\CLSIDFromString", "str", str, "ptr", buf := Buffer(16), "hresult") 324 | return buf 325 | } 326 | 327 | static getRect(buf, &left, &top, &right, &bottom) { 328 | left := NumGet(buf, 0, "int") 329 | top := NumGet(buf, 4, "int") 330 | right := NumGet(buf, 8, "int") 331 | bottom := NumGet(buf, 12, "int") 332 | } 333 | 334 | static getWindowScale(hwnd) { 335 | if winDpi := DllCall("GetDpiForWindow", "ptr", hwnd, "uint") 336 | return A_ScreenDPI / winDpi 337 | return 1 338 | } 339 | 340 | static scaleRect(scale, &left, &top, &right, &bottom) { 341 | left := Round(left * scale) 342 | top := Round(top * scale) 343 | right := Round(right * scale) 344 | bottom := Round(bottom * scale) 345 | } 346 | 347 | static clientToScreenRect(hwnd, &left, &top, &right, &bottom) { 348 | w := right - left 349 | h := bottom - top 350 | pt := left | top << 32 351 | DllCall("ClientToScreen", "ptr", hwnd, "int64*", &pt) 352 | left := pt & 0xffffffff 353 | top := pt >> 32 354 | right := left + w 355 | bottom := top + h 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /Rabbit.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 - 2025 Xuesong Peng 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | #Requires AutoHotkey v2.0 19 | #SingleInstance Ignore 20 | 21 | ;@Ahk2Exe-SetInternalName rabbit 22 | ;@Ahk2Exe-SetProductName 玉兔毫 23 | ;@Ahk2Exe-SetOrigFilename Rabbit.ahk 24 | 25 | #Include 26 | #Include 27 | #Include 28 | #Include 29 | #Include 30 | #Include 31 | #Include 32 | #Include 33 | 34 | global IN_MAINTENANCE := false 35 | global session_id := 0 36 | global mutex := RabbitMutex() 37 | global last_is_hide := false 38 | 39 | RabbitMain(A_Args) 40 | 41 | ; args[1]: maintenance option 42 | ; args[2]: deployer result 43 | ; args[3]: keyboard layout 44 | RabbitMain(args) { 45 | global box, rabbit_traits 46 | if args.Length >= 3 47 | layout := Number(args[3]) 48 | if !IsSet(layout) || layout == 0 { 49 | layout := DllCall("GetKeyboardLayout", "UInt", 0) 50 | } 51 | RabbitGlobals.keyboard_layout := layout 52 | SetDefaultKeyboard() 53 | 54 | fail_count := 0 55 | while not mutex.Create() { 56 | mutex.Close() 57 | fail_count++ 58 | if fail_count > 500 { 59 | TrayTip() 60 | TrayTip("有其他进程正在使用 RIME,启动失败") 61 | Sleep(2000) 62 | ExitApp() 63 | } 64 | } 65 | 66 | ; TODO: better handling of first run 67 | local first_run := !FileExist(RabbitUserDataPath() . "\default.custom.yaml") 68 | || !FileExist(RabbitUserDataPath() . "\rabbit.custom.yaml") 69 | || !FileExist(RabbitUserDataPath() . "\user.yaml") 70 | || !FileExist(RabbitUserDataPath() . "\installation.yaml") 71 | || !FileExist(RabbitUserDataPath() . "\build\rabbit.yaml") ; in staging dir 72 | 73 | rabbit_traits := CreateTraits() 74 | global rime 75 | rime.setup(rabbit_traits) 76 | rime.set_notification_handler(OnRimeMessage, 0) 77 | rime.initialize(rabbit_traits) 78 | 79 | local m := (args.Length == 0) ? RABBIT_PARTIAL_MAINTENANCE : args[1] 80 | if m != RABBIT_NO_MAINTENANCE { 81 | global IN_MAINTENANCE := true 82 | UpdateTrayIcon() 83 | if first_run { 84 | RunDeployer("install", RabbitGlobals.keyboard_layout) 85 | } else if rime.start_maintenance(m == RABBIT_FULL_MAINTENANCE) 86 | rime.join_maintenance_thread() 87 | } else { 88 | TrayTip() 89 | TrayTip("维护完成", RABBIT_IME_NAME) 90 | SetTimer(TrayTip, -2000) 91 | } 92 | IN_MAINTENANCE := false 93 | 94 | global session_id := rime.create_session() 95 | if not session_id { 96 | SetDefaultKeyboard(RabbitGlobals.keyboard_layout) 97 | rime.finalize() 98 | throw Error("未能成功创建 RIME 会话。") 99 | } 100 | 101 | CleanOldLogs() 102 | CleanMisPlacedConfigs() 103 | RabbitConfig.load() 104 | if RabbitConfig.use_legacy_candidate_box 105 | box := LegacyCandidateBox() 106 | else 107 | box := CandidateBox() 108 | RegisterHotKeys() 109 | UpdateStateLabels() 110 | if status := rime.get_status(session_id) { 111 | local schema_id := status.schema_id 112 | local schema_name := status.schema_name 113 | local ascii_mode := status.is_ascii_mode 114 | local full_shape := status.is_full_shape 115 | local ascii_punct := status.is_ascii_punct 116 | rime.free_status(status) 117 | 118 | UpdateTrayTip(schema_name, ascii_mode, full_shape, ascii_punct) 119 | 120 | if RabbitConfig.schema_icon.Has(schema_id) 121 | if RabbitGlobals.current_schema_icon := RabbitConfig.schema_icon[schema_id] 122 | UpdateTrayIcon() 123 | } 124 | SetupTrayMenu() 125 | box.UpdateUIStyle() 126 | OnMessage(AHK_NOTIFYICON, ClickHandler.Bind()) 127 | OnMessage(WM_SETTINGCHANGE, OnColorChange.Bind()) 128 | OnMessage(WM_DWMCOLORIZATIONCOLORCHANGED, OnColorChange.Bind()) 129 | if !RabbitConfig.global_ascii 130 | SetTimer(UpdateWinAscii) 131 | 132 | OnExit(ExitRabbit.Bind(RabbitGlobals.keyboard_layout)) 133 | } 134 | 135 | ; https://www.autohotkey.com/boards/viewtopic.php?f=76&t=101183 136 | SetDefaultKeyboard(locale_id := 0x0409) { 137 | if FileExist(RabbitUserDataPath() . "\.lang") 138 | return 139 | local locale_id_hex := Format("{:08x}", locale_id & 0xffff) 140 | lang := DllCall("LoadKeyboardLayout", "Str", locale_id_hex, "Int", 0) 141 | PostMessage(WM_INPUTLANGCHANGEREQUEST := 0x0050, 0, lang, HWND_BROADCAST := 0xffff) 142 | } 143 | 144 | ExitRabbit(layout, reason, code) { 145 | if code == 0 146 | SetDefaultKeyboard(layout) 147 | TrayTip() 148 | ToolTip(, , , STATUS_TOOLTIP) 149 | if session_id { 150 | rime.destroy_session(session_id) 151 | rime.finalize() 152 | } 153 | if mutex 154 | mutex.Close() 155 | } 156 | 157 | RegisterHotKeys() { 158 | global rime 159 | global suspend_hotkey_mask := 0 160 | global suspend_hotkey := "" 161 | local shift := KeyDef.mask["Shift"] 162 | local ctrl := KeyDef.mask["Ctrl"] 163 | local alt := KeyDef.mask["Alt"] 164 | local win := KeyDef.mask["Win"] 165 | local up := KeyDef.mask["Up"] 166 | 167 | ; Modifiers 168 | for modifier, _ in KeyDef.modifier_code { 169 | if modifier == "LWin" or modifier == "RWin" or modifier == "LAlt" or modifier == "RAlt" 170 | continue ; do not register Win / Alt keys for now 171 | local mask := KeyDef.mask[modifier] 172 | Hotkey("$" . modifier, ProcessKey.Bind(modifier, mask), "S0") 173 | Hotkey("$" . modifier . " Up", ProcessKey.Bind(modifier, mask | up), "S0") 174 | } 175 | 176 | ; Plain 177 | Loop 2 { 178 | local key_map := A_Index = 1 ? KeyDef.plain_keycode : KeyDef.other_keycode 179 | for key, _ in key_map { 180 | Hotkey("$" . key, ProcessKey.Bind(key, 0), "S0") 181 | ; need specify left/right to prevent fallback to modifier down/up hotkeys 182 | Hotkey("$<^" . key, ProcessKey.Bind(key, ctrl), "S0") 183 | ; do not register Alt + single key now 184 | ; if not key = "Tab" { 185 | ; Hotkey("$!" . key, ProcessKey.Bind(key, alt), "S0") 187 | ; } 188 | Hotkey("$>^" . key, ProcessKey.Bind(key, ctrl), "S0") 189 | Hotkey("$^!" . key, ProcessKey.Bind(key, ctrl | alt), "S0") 190 | Hotkey("$!#" . key, ProcessKey.Bind(key, alt | win), "S0") 191 | 192 | ; Do not register Win keys for now 193 | ; Hotkey("$<#" . key, ProcessKey.Bind(key, win), "S0") 194 | ; Hotkey("$>#" . key, ProcessKey.Bind(key, win), "S0") 195 | ; Hotkey("$^#" . key, ProcessKey.Bind(key, ctrl | win), "S0") 196 | ; Hotkey("$^!#" . key, ProcessKey.Bind(key, ctrl | alt | win), "S0") 197 | } 198 | } 199 | 200 | ; Shifted 201 | Loop 2 { 202 | local key_map := A_Index = 1 ? KeyDef.shifted_keycode : KeyDef.other_keycode 203 | for key, _ in key_map { 204 | Hotkey("$<+" . key, ProcessKey.Bind(key, shift), "S0") 205 | Hotkey("$>+" . key, ProcessKey.Bind(key, shift), "S0") 206 | Hotkey("$+^" . key, ProcessKey.Bind(key, shift | ctrl), "S0") 207 | if not key == "Tab" 208 | Hotkey("$+!" . key, ProcessKey.Bind(key, shift | alt), "S0") 209 | Hotkey("$+^!" . key, ProcessKey.Bind(key, shift | ctrl | alt), "S0") 210 | 211 | ; Do not register Win keys for now 212 | ; Hotkey("$+#" . key, ProcessKey.Bind(key, shift | win), "S0") 213 | ; Hotkey("$+^#" . key, ProcessKey.Bind(key, shift | ctrl | win), "S0") 214 | ; Hotkey("$+!#" . key, ProcessKey.Bind(key, shift | alt | win), "S0") 215 | ; Hotkey("$+^!#" . key, ProcessKey.Bind(key, shift | ctrl | alt | win), "S0") 216 | } 217 | } 218 | 219 | ; Special handling 220 | Hotkey("$Space Up", ProcessKey.Bind("Space", up), "S0") 221 | 222 | ; Read the hotkey to suspend / resume Rabbit 223 | if !RabbitConfig.suspend_hotkey 224 | return 225 | local keys := StrSplit(RabbitConfig.suspend_hotkey, "+", " ", 4) 226 | local mask := 0 227 | local target_key := "" 228 | local num_modifiers := 0 229 | for k in keys { 230 | if k = "Control" { 231 | num_modifiers += !(mask & ctrl) 232 | mask |= ctrl 233 | } else if k = "Alt" { 234 | num_modifiers += !(mask & alt) 235 | mask |= alt 236 | } else if k = "Shift" { 237 | num_modifiers += !(mask & shift) 238 | mask |= shift 239 | } else if not target_key { 240 | target_key := k 241 | } 242 | } 243 | 244 | if target_key { 245 | if KeyDef.rime_to_ahk.Has(target_key) 246 | target_key := KeyDef.rime_to_ahk[target_key] 247 | if num_modifiers = 1 { 248 | if mask & ctrl { 249 | Hotkey("$<^" . target_key, , "S") 250 | Hotkey("$>^" . target_key, , "S") 251 | suspend_hotkey_mask := mask 252 | suspend_hotkey := target_key 253 | } 254 | } else if num_modifiers > 1 { 255 | local m := "$" . (mask & shift ? "+" : "") . 256 | (mask & ctrl ? "^" : "") . 257 | (mask & alt ? "!" : "") 258 | Hotkey(m . target_key, , "S") 259 | suspend_hotkey_mask := mask 260 | suspend_hotkey := target_key 261 | } 262 | } else if keys.Length == 1 { 263 | if keys[1] = "Shift" { 264 | ; do not support now 265 | Hotkey("$LShift", , "S") 266 | Hotkey("$RShift", , "S") 267 | Hotkey("$LShift Up", , "S") 268 | Hotkey("$RShift Up", , "S") 269 | suspend_hotkey_mask := mask | up 270 | suspend_hotkey := "Shift" 271 | } 272 | } 273 | } 274 | 275 | ProcessKey(key, mask, this_hotkey) { 276 | global suspend_hotkey_mask, suspend_hotkey 277 | global last_is_hide 278 | local code := 0 279 | Loop 4 { 280 | local key_map 281 | switch A_Index { 282 | case 1: 283 | key_map := KeyDef.modifier_code 284 | case 2: 285 | key_map := KeyDef.plain_keycode 286 | case 3: 287 | key_map := KeyDef.shifted_keycode 288 | case 4: 289 | key_map := KeyDef.other_keycode 290 | default: 291 | return 292 | } 293 | for check_key, check_code in key_map { 294 | if key == check_key { 295 | code := check_code 296 | break 297 | } 298 | } 299 | if code 300 | break 301 | } 302 | if not code 303 | return 304 | 305 | if caps := GetKeyState("CapsLock", "T") { 306 | if StrLen(key) == 1 and Ord(key) >= Ord("a") and Ord(key) <= Ord("z") ; small case letters 307 | code += (Ord("A") - Ord("a")) 308 | } 309 | 310 | if status := rime.get_status(session_id) { 311 | local old_schema_id := status.schema_id 312 | local old_ascii_mode := status.is_ascii_mode 313 | local old_full_shape := status.is_full_shape 314 | local old_ascii_punct := status.is_ascii_punct 315 | rime.free_status(status) 316 | } 317 | 318 | processed := rime.process_key(session_id, code, mask) 319 | 320 | status := rime.get_status(session_id) 321 | local new_schema_id := status.schema_id 322 | local new_schema_name := status.schema_name 323 | local new_ascii_mode := status.is_ascii_mode 324 | local new_full_shape := status.is_full_shape 325 | local new_ascii_punct := status.is_ascii_punct 326 | rime.free_status(status) 327 | 328 | if old_schema_id !== new_schema_id { 329 | UpdateStateLabels() 330 | } 331 | 332 | UpdateTrayTip(new_schema_name, new_ascii_mode, new_full_shape, new_ascii_punct) 333 | if old_schema_id !== new_schema_id && RabbitConfig.schema_icon.Has(new_schema_id) { 334 | if RabbitGlobals.current_schema_icon := RabbitConfig.schema_icon[new_schema_id] 335 | UpdateTrayIcon() 336 | } 337 | 338 | local status_text := "" 339 | local status_changed := false 340 | local ascii_changed := false 341 | if old_ascii_mode != new_ascii_mode { 342 | ascii_changed := true 343 | UpdateWinAscii(new_ascii_mode, true) 344 | status_text := new_ascii_mode ? ASCII_MODE_TRUE_LABEL_ABBR : ASCII_MODE_FALSE_LABEL_ABBR 345 | } else if old_full_shape != new_full_shape { 346 | status_changed := true 347 | status_text := new_full_shape ? FULL_SHAPE_TRUE_LABEL_ABBR : FULL_SHAPE_FALSE_LABEL_ABBR 348 | } else if old_ascii_punct != new_ascii_punct { 349 | status_changed := true 350 | status_text := new_ascii_punct ? ASCII_PUNCT_TRUE_LABEL_ABBR : ASCII_PUNCT_FALSE_LABEL_ABBR 351 | } 352 | 353 | if RabbitConfig.show_tips && (status_changed || ascii_changed) { 354 | ToolTip(status_text, , , STATUS_TOOLTIP) 355 | SetTimer(() => ToolTip(, , , STATUS_TOOLTIP), -RabbitConfig.show_tips_time) 356 | } 357 | 358 | if commit := rime.get_commit(session_id) { 359 | if ascii_changed 360 | last_is_hide := true 361 | else 362 | last_is_hide := false 363 | if StrLen(commit.text) >= RabbitConfig.send_by_clipboard_length 364 | SendTextByClipboard(commit.text) 365 | else 366 | SendText(commit.text) 367 | box.Hide() 368 | rime.free_commit(commit) 369 | } else 370 | last_is_hide := false 371 | 372 | if (suspend_hotkey and suspend_hotkey_mask) 373 | and (key = suspend_hotkey or SubStr(key, 2) = suspend_hotkey) 374 | and (mask = suspend_hotkey_mask) { 375 | ToggleSuspend() 376 | return 377 | } 378 | 379 | if context := rime.get_context(session_id) { 380 | static prev_show := false 381 | static prev_x := 4 382 | static prev_y := 4 383 | if (context.composition.length > 0 or context.menu.num_candidates > 0) { 384 | DetectHiddenWindows True 385 | local start_menu := WinActive("ahk_class Windows.UI.Core.CoreWindow ahk_exe StartMenuExperienceHost.exe") 386 | || WinActive("ahk_class Windows.UI.Core.CoreWindow ahk_exe SearchHost.exe") 387 | || WinActive("ahk_class Windows.UI.Core.CoreWindow ahk_exe SearchApp.exe") 388 | DetectHiddenWindows False 389 | local show_at_left_top := false 390 | if start_menu { 391 | hMon := MonitorManage.MonitorFromWindow(start_menu) 392 | info := MonitorManage.GetMonitorInfo(hMon) 393 | show_at_left_top := !!info 394 | if show_at_left_top && !last_is_hide { 395 | box.Build(context, &box_width, &box_height) 396 | box.Show(info.work.left + 4, info.work.top + 4) 397 | } 398 | } 399 | if !show_at_left_top && GetCaretPos(&caret_x, &caret_y, &caret_w, &caret_h) { 400 | box.Build(context, &box_width, &box_height) 401 | if RabbitConfig.fix_candidate_box && prev_show { 402 | new_x := prev_x 403 | new_y := prev_y 404 | } else { 405 | new_x := caret_x + caret_w 406 | new_y := caret_y + caret_h + 4 407 | 408 | hWnd := WinExist("A") 409 | hMon := MonitorManage.MonitorFromWindow(hWnd) 410 | info := MonitorManage.GetMonitorInfo(hMon) 411 | if info { 412 | if new_x + box_width > info.work.right 413 | new_x := info.work.right - box_width 414 | if new_y + box_height > info.work.bottom 415 | new_y := caret_y - 4 - box_height 416 | } else { 417 | workspace_width := SysGet(16) ; SM_CXFULLSCREEN 418 | workspace_height := SysGet(17) ; SM_CYFULLSCREEN 419 | if new_x + box_width > workspace_width 420 | new_x := workspace_width - box_width 421 | if new_y + box_height > workspace_height 422 | new_y := caret_y - 4 - box_height 423 | } 424 | } 425 | if !last_is_hide 426 | box.Show(new_x, new_y) 427 | prev_x := new_x 428 | prev_y := new_y 429 | } else if !show_at_left_top { 430 | backup_mouse_ref := A_CoordModeMouse 431 | CoordMode("Mouse", "Screen") 432 | MouseGetPos(&mouse_x, &mouse_y) 433 | CoordMode("Mouse", backup_mouse_ref) 434 | box.Build(context, &box_width, &box_height) 435 | box.Show(mouse_x, mouse_y) 436 | } 437 | prev_show := true 438 | } else { 439 | box.Hide() 440 | prev_show := false 441 | } 442 | rime.free_context(context) 443 | } 444 | 445 | if not processed { 446 | local shift := (mask & KeyDef.mask["Shift"]) ? "+" : "" 447 | local ctrl := (mask & KeyDef.mask["Ctrl"]) ? "^" : "" 448 | local alt := (mask & KeyDef.mask["Alt"]) ? "!" : "" 449 | local win := (mask & KeyDef.mask["Win"]) ? "#" : "" 450 | 451 | local isUp := mask & KeyDef.mask["Up"] 452 | local hasModifier := mask & (KeyDef.mask["Shift"] | KeyDef.mask["Ctrl"] | KeyDef.mask["Alt"] | KeyDef.mask["Win"]) 453 | 454 | if key == "Space" and not hasModifier { 455 | Send("{Blind}{" . key . (isUp ? " Up" : " Down") . "}") 456 | } else { 457 | SendInput(shift . ctrl . alt . win . "{" . key . "}") 458 | } 459 | } 460 | } 461 | 462 | UpdateStateLabels() { 463 | global rime, session_id, ASCII_MODE_FALSE_LABEL, ASCII_MODE_TRUE_LABEL, ASCII_MODE_FALSE_LABEL_ABBR, ASCII_MODE_TRUE_LABEL_ABBR, FULL_SHAPE_FALSE_LABEL, FULL_SHAPE_TRUE_LABEL, FULL_SHAPE_FALSE_LABEL_ABBR, FULL_SHAPE_TRUE_LABEL_ABBR, ASCII_PUNCT_FALSE_LABEL, ASCII_PUNCT_TRUE_LABEL, ASCII_PUNCT_FALSE_LABEL_ABBR, ASCII_PUNCT_TRUE_LABEL_ABBR 464 | if not rime 465 | return 466 | 467 | str := rime.get_state_label(session_id, "ascii_mode", false) 468 | ASCII_MODE_FALSE_LABEL := str ? str : "中文" 469 | str := rime.get_state_label(session_id, "ascii_mode", true) 470 | ASCII_MODE_TRUE_LABEL := str ? str : "西文" 471 | slice := rime.get_state_label_abbreviated(session_id, "ascii_mode", false, true) 472 | ASCII_MODE_FALSE_LABEL_ABBR := (slice and slice.slice !== "") ? slice.slice : "中" 473 | slice := rime.get_state_label_abbreviated(session_id, "ascii_mode", true, true) 474 | ASCII_MODE_TRUE_LABEL_ABBR := (slice and slice.slice !== "") ? slice.slice : "西" 475 | str := rime.get_state_label(session_id, "full_shape", false) 476 | FULL_SHAPE_FALSE_LABEL := str ? str : "半角" 477 | str := rime.get_state_label(session_id, "full_shape", true) 478 | FULL_SHAPE_TRUE_LABEL := str ? str : "全角" 479 | slice := rime.get_state_label_abbreviated(session_id, "full_shape", false, true) 480 | FULL_SHAPE_FALSE_LABEL_ABBR := (slice and slice.slice !== "") ? slice.slice : "半" 481 | slice := rime.get_state_label_abbreviated(session_id, "full_shape", true, true) 482 | FULL_SHAPE_TRUE_LABEL_ABBR := (slice and slice.slice !== "") ? slice.slice : "全" 483 | str := rime.get_state_label(session_id, "ascii_punct", false) 484 | ASCII_PUNCT_FALSE_LABEL := str ? str : "。," 485 | str := rime.get_state_label(session_id, "ascii_punct", true) 486 | ASCII_PUNCT_TRUE_LABEL := str ? str : ". ," 487 | slice := rime.get_state_label_abbreviated(session_id, "ascii_punct", false, true) 488 | ASCII_PUNCT_FALSE_LABEL_ABBR := (slice and slice.slice !== "") ? slice.slice : "。" 489 | slice := rime.get_state_label_abbreviated(session_id, "ascii_punct", true, true) 490 | ASCII_PUNCT_TRUE_LABEL_ABBR := (slice and slice.slice !== "") ? slice.slice : "." 491 | } 492 | 493 | UpdateWinAscii(target := false, use_target := false, proc_name := "", by_tray_icon := false) { 494 | if A_IsSuspended 495 | return 496 | if RabbitGlobals.on_tray_icon_click && !by_tray_icon 497 | return 498 | global rime, session_id 499 | if !rime || !session_id 500 | return 501 | if not proc_name { 502 | if not act := WinExist("A") 503 | return 504 | try { 505 | proc_name := StrLower(WinGetProcessName()) 506 | } 507 | if not proc_name 508 | return 509 | } 510 | RabbitGlobals.active_win := proc_name 511 | ; TODO: current state might not be accurate due to non-atomic 512 | current := !!rime.get_option(session_id, "ascii_mode") 513 | if use_target { 514 | ; force to use passed target 515 | RabbitGlobals.process_ascii[proc_name] := !!target 516 | } else if RabbitGlobals.process_ascii.Has(proc_name) { 517 | ; not first time to active window, restore the ascii_mode 518 | target := RabbitGlobals.process_ascii[proc_name] 519 | if current !== target 520 | rime.set_option(session_id, "ascii_mode", target) 521 | } else if RabbitConfig.preset_process_ascii.Has(proc_name) { 522 | ; in preset, set ascii_mode as preset 523 | target := RabbitConfig.preset_process_ascii[proc_name] 524 | RabbitGlobals.process_ascii[proc_name] := !!target 525 | if current !== target 526 | rime.set_option(session_id, "ascii_mode", target) 527 | } else { 528 | ; not in preset, set ascii_mode to false 529 | target := false 530 | RabbitGlobals.process_ascii[proc_name] := !!target 531 | if current !== target 532 | rime.set_option(session_id, "ascii_mode", target) 533 | } 534 | UpdateTrayTip(, target) 535 | UpdateTrayIcon() 536 | } 537 | 538 | ; by rawbx (https://github.com/rimeinn/rabbit/issues/13#issuecomment-3072554342) 539 | SendTextByClipboard(text) { 540 | clip_prev := A_Clipboard 541 | A_Clipboard := text 542 | 543 | if ClipWait(0.5, 0) 544 | Send('+{Insert}') ; or Send('^v') 545 | 546 | ; Restore clipboard 547 | SetTimer(() => A_Clipboard := clip_prev, -50) 548 | } 549 | -------------------------------------------------------------------------------- /RabbitDeployer.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 - 2025 Xuesong Peng 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | #Requires AutoHotkey v2.0 19 | #SingleInstance Ignore 20 | 21 | ;@Ahk2Exe-SetInternalName rabbit-deployer 22 | ;@Ahk2Exe-SetProductName 玉兔毫部署应用 23 | ;@Ahk2Exe-SetOrigFilename RabbitDeployer.ahk 24 | 25 | #Include 26 | #Include 27 | #Include 28 | 29 | ;@Ahk2Exe-SetMainIcon Lib\rabbit-alt.ico 30 | global IN_MAINTENANCE := true 31 | global rime 32 | global INVALID_FILE_ATTRIBUTES := -1 33 | global FILE_ATTRIBUTE_DIRECTORY := 0x00000010 34 | 35 | OnExit(ExitRabbitDeployer) 36 | 37 | RabbitDeployerMain(A_Args) 38 | 39 | ; args[1]: command 40 | ; args[2]: keyboard layout 41 | RabbitDeployerMain(args) { 42 | if args.Length >= 2 43 | layout := Number(args[2]) 44 | else 45 | layout := 0 46 | IN_MAINTENANCE := true 47 | UpdateTrayIcon() 48 | TrayTip() 49 | TrayTip("维护中", RABBIT_IME_NAME) 50 | SetupTrayMenu() 51 | 52 | command := args.Length > 0 ? args[1] : "" 53 | conf := Configurator() 54 | conf.Initialize() 55 | switch command { 56 | case "deploy": 57 | res := conf.UpdateWorkspace() 58 | opt := RABBIT_NO_MAINTENANCE 59 | case "dict": 60 | res := conf.DictManagement() 61 | opt := RABBIT_PARTIAL_MAINTENANCE 62 | case "sync": 63 | res := conf.SyncUserData() 64 | opt := RABBIT_PARTIAL_MAINTENANCE 65 | default: 66 | res := conf.Run(command = "install") 67 | opt := RABBIT_NO_MAINTENANCE 68 | } 69 | 70 | if args.Length > 1 { 71 | if A_IsCompiled 72 | Run(Format("`"{}\Rabbit.exe`" {} {} {}", A_ScriptDir, opt, res, layout)) 73 | else 74 | Run(Format("{} `"{}\Rabbit.ahk`" {} {} {}", A_AhkPath, A_ScriptDir, opt, res, layout)) 75 | ExitApp() 76 | } 77 | return res 78 | } 79 | 80 | ExitRabbitDeployer(reason, code) { 81 | TrayTip() 82 | } 83 | 84 | CreateFileIfNotExist(filename) { 85 | user_data_dir := RabbitUserDataPath() . "\" 86 | if not InStr(DirExist(user_data_dir), "D") 87 | DirCreate(user_data_dir) 88 | filepath := user_data_dir . filename 89 | if not InStr(FileExist(filepath), "N") 90 | FileAppend("", filepath) 91 | } 92 | 93 | ConfigureSwitcher(levers, switcher_settings, &reconfigured) { 94 | if !IsSet(reconfigured) 95 | reconfigured := false 96 | if not levers.load_settings(switcher_settings) 97 | return false 98 | ; To mimic a dialog 99 | result := { 100 | yes : false 101 | } 102 | dialog := SwitcherSettingsDialog(switcher_settings, result) 103 | dialog.Show() 104 | WinWaitClose(dialog) 105 | 106 | if result.yes { 107 | if levers.save_settings(switcher_settings) 108 | reconfigured := true 109 | return true 110 | } 111 | return false 112 | } 113 | 114 | ConfigureUI(levers, ui_style_settings, &reconfigured) { 115 | if !IsSet(reconfigured) 116 | reconfigured := false 117 | local settings := ui_style_settings.settings 118 | if !levers.load_settings(settings) 119 | return false 120 | result := { 121 | yes : false 122 | } 123 | dialog := UIStyleSettingsDialog(ui_style_settings, result) 124 | dialog.Show() 125 | WinWaitClose(dialog) 126 | 127 | if result.yes { 128 | if levers.save_settings(settings) 129 | reconfigured := true 130 | return true 131 | } 132 | return false 133 | } 134 | 135 | class Configurator extends Class { 136 | __New() { 137 | CreateFileIfNotExist("default.custom.yaml") 138 | CreateFileIfNotExist("rabbit.custom.yaml") 139 | } 140 | 141 | Initialize() { 142 | global rabbit_traits 143 | rabbit_traits := CreateTraits() 144 | rime.setup(rabbit_traits) 145 | rime.deployer_initialize(0) 146 | } 147 | 148 | Run(installing) { 149 | levers := RimeLeversApi() 150 | if not levers 151 | return 1 152 | 153 | switcher_settings := levers.switcher_settings_init() 154 | ui_style_settings := UIStyleSettings() 155 | skip_switcher_settings := installing && !levers.is_first_run(switcher_settings) 156 | skip_ui_style_settings := installing && !levers.is_first_run(ui_style_settings.settings) 157 | 158 | if !skip_switcher_settings { 159 | if !ConfigureSwitcher(levers, switcher_settings, &reconfigured) 160 | skip_ui_style_settings := true ; user cancelled 161 | } 162 | if !skip_ui_style_settings 163 | ConfigureUI(levers, ui_style_settings, &reconfigured) 164 | 165 | levers.custom_settings_destroy(switcher_settings) 166 | 167 | if installing || reconfigured 168 | return this.UpdateWorkspace() 169 | 170 | return 0 171 | } 172 | 173 | UpdateWorkspace(report_errors := false) { 174 | mutex := RabbitMutex() 175 | if not mutex.Create() { 176 | ; TODO: log error 177 | return 1 178 | } 179 | 180 | if mutex.lasterr == ERROR_ALREADY_EXISTS { 181 | ; TODO: log error 182 | mutex.Close() 183 | if report_errors { 184 | MsgBox("正在执行另一项部署任务,方才所做的修改将在输入法再次启动后生效。", "【玉兔毫】", "Ok Iconi") 185 | } 186 | return 1 187 | } 188 | 189 | { 190 | rime.deploy() 191 | rime.deploy_config_file("rabbit.yaml", "config_version") 192 | } 193 | 194 | mutex.Close() 195 | 196 | return 0 197 | } 198 | 199 | DictManagement() { 200 | mutex := RabbitMutex() 201 | if not mutex.Create() { 202 | ; TODO: log error 203 | return 1 204 | } 205 | 206 | if mutex.lasterr == ERROR_ALREADY_EXISTS { 207 | ; TODO: log error 208 | mutex.Close() 209 | MsgBox("正在执行另一项部署任务,请稍后再试。", "【玉兔毫】", "Ok Iconi") 210 | return 1 211 | } 212 | 213 | { 214 | if rime.api_available("run_task") { 215 | rime.run_task("installation_update") 216 | } 217 | dialog := DictManagementDialog() 218 | dialog.Show() 219 | WinWaitClose(dialog) 220 | } 221 | 222 | mutex.Close() 223 | 224 | return 0 225 | } 226 | 227 | SyncUserData() { 228 | mutex := RabbitMutex() 229 | if not mutex.Create() { 230 | ; TODO: log error 231 | return 1 232 | } 233 | 234 | if mutex.lasterr == ERROR_ALREADY_EXISTS { 235 | ; TODO: log error 236 | mutex.Close() 237 | MsgBox("正在执行另一项部署任务,请稍后再试。", "【玉兔毫】", "Ok Iconi") 238 | return 1 239 | } 240 | 241 | { 242 | if not rime.sync_user_data() { 243 | mutex.Close() 244 | return 1 245 | } 246 | rime.join_maintenance_thread() 247 | } 248 | 249 | mutex.Close() 250 | 251 | return 0 252 | } 253 | } 254 | 255 | class DictManagementDialog extends Gui { 256 | __New() { 257 | super.__New("-MaximizeBox -MinimizeBox", "【玉兔毫】用户词典管理", this) 258 | this.api := RimeLeversApi() 259 | 260 | ; Layout 261 | this.MarginX := 15 262 | this.MarginY := 15 263 | this.AddText(, "用户词典列表:") 264 | this.dict_list := this.AddListBox("w190 h270", []) 265 | this.dict_list.OnEvent("Change", (*) => this.OnUserDictListSelChange()) 266 | this.AddText("Section X+25 YP w315", " 当你需要将包含输入习惯的用户词典迁移到另一份配备了 Rime 输入法的系统,请在左列选中词典名称,「输出词典快照」将快照文件传到另一系统上,「合入词典快照」快照文件中的词条将合并到其所属的词典中。") 267 | this.backup := this.AddButton("Disabled Y+30 w150", "输出词典快照") 268 | this.backup.OnEvent("Click", (*) => this.OnBackup()) 269 | this.AddButton("X+20 YP w150", "合入词典快照").OnEvent("Click", (*) => this.OnRestore()) 270 | this.AddText("XS w315", "「导出文本码表」是为输入方案制作者设计的功能,将使用期间新造的词组以 Rime 词典中的码表格式导出,以便查看、编辑。「导入文本码表」可用于将其他来源的词库整理成 TSV 格式后导入到 Rime。在 Rime 输入法之间转移数据,请使用词典快照。") 271 | this.export := this.AddButton("Disabled Y+30 w150", "导出文本码表") 272 | this.export.OnEvent("Click", (*) => this.OnExport()) 273 | this.import := this.AddButton("Disabled X+20 YP w150", "导入文本码表") 274 | this.import.OnEvent("Click", (*) => this.OnImport()) 275 | 276 | this.Populate() 277 | } 278 | 279 | Populate() { 280 | if !iter := this.api.user_dict_iterator_init() { 281 | return 282 | } 283 | while dict := this.api.next_user_dict(iter) { 284 | this.dict_list.Add([dict]) 285 | } 286 | this.api.user_dict_iterator_destroy(iter) 287 | this.dict_list.Choose(0) 288 | } 289 | 290 | OnBackup() { 291 | local sel := this.dict_list.Value 292 | if sel <= 0 || sel > ControlGetItems(this.dict_list).Length { 293 | MsgBox("请在左列选择要导出的词典名称。", ":-(", "Ok Iconi") 294 | return 295 | } 296 | 297 | local path := rime.get_user_data_sync_dir() 298 | if !DirExist(path) { 299 | try { 300 | DirCreate(path) 301 | } catch { 302 | MsgBox("未能完成导出操作。会不会是同步文件夹无法访问?", ":-(", "Ok Iconx") 303 | return 304 | } 305 | } 306 | 307 | local dict_name := this.dict_list.Text 308 | file := path . "\" . dict_name . ".userdb.txt" 309 | if !this.api.backup_user_dict(dict_name) { 310 | MsgBox("不知哪里出错了,未能完成导出操作。", ":-(", "Ok Iconx") 311 | return 312 | } else if !FileExist(file) { 313 | MsgBox("咦,输出的快照文件找不着了。", ":-(", "Ok Iconx") 314 | return 315 | } 316 | Run(A_ComSpec . " /c explorer.exe /select,`"" . file . "`"", , "Hide") 317 | } 318 | 319 | OnRestore() { 320 | local filter := "词典快照 (*.userdb.txt; *.userdb.kct.snapshot)" 321 | if selected_path := FileSelect("1", , "打开", filter) { ; file must exist 322 | if !this.api.restore_user_dict(selected_path) 323 | MsgBox("不知哪里出错了,未能完成操作。", ":-(", "Ok Iconx") 324 | else 325 | MsgBox("完成了。", ":-)", "Ok Iconi") 326 | } 327 | } 328 | 329 | OnExport() { 330 | local sel := this.dict_list.Value 331 | if sel <= 0 || sel > ControlGetItems(this.dict_list).Length { 332 | MsgBox("请在左列选择要导出的词典名称。", ":-(", "Ok Iconi") 333 | return 334 | } 335 | 336 | local dict_name := this.dict_list.Text 337 | local file_name := dict_name . "_export.txt" 338 | local filter := "文本文档 (*.txt)" 339 | if selected_path := FileSelect("S18", file_name, "另存为", filter) { ; path must exist + warning on overwriting 340 | if SubStr(selected_path, -4) != ".txt" 341 | selected_path .= ".txt" 342 | local result := this.api.export_user_dict(dict_name, selected_path) 343 | if result < 0 344 | MsgBox("不知哪里出错了,未能完成操作。", ":-(", "Ok Iconx") 345 | else if !FileExist(selected_path) 346 | MsgBox("咦,导出的文件找不着了。", ":-(", "Ok Iconx") 347 | else { 348 | MsgBox("导出了 " . result . " 条记录。", ":-)", "Ok Iconi") 349 | Run(A_ComSpec . " /c explorer.exe /select,`"" . selected_path . "`"", , "Hide") 350 | } 351 | } 352 | } 353 | 354 | OnImport() { 355 | local dict_name := this.dict_list.Text 356 | local file_name := dict_name . "_export.txt" 357 | local filter := "文本文档 (*.txt)" 358 | if selected_path := FileSelect("1", file_name, "打开", filter) { ; file must exist 359 | local result := this.api.import_user_dict(dict_name, selected_path) 360 | if result < 0 361 | MsgBox("不知哪里出错了,未能完成操作。", ":-(", "Ok Iconx") 362 | else 363 | MsgBox("导入了 " . result . " 条记录。", ":-)", "Ok Iconi") 364 | } 365 | } 366 | 367 | OnUserDictListSelChange() { 368 | local index := this.dict_list.Value 369 | local enabled := index <= 0 ? false : true 370 | this.backup.Enabled := enabled 371 | this.export.Enabled := enabled 372 | this.import.Enabled := enabled 373 | } 374 | } 375 | 376 | class SwitcherSettingsDialog extends Gui { 377 | __New(settings, result) { 378 | super.__New("-MaximizeBox -MinimizeBox", "【玉兔毫】方案选单设定", this) 379 | this.settings := settings 380 | this.loaded := false 381 | this.modified := false 382 | this.api := RimeLeversApi() 383 | 384 | this.item_data := Map() 385 | this.result := result 386 | 387 | ; Layout 388 | this.MarginX := 15 389 | this.MarginY := 15 390 | this.AddText(, "请勾选所需的输入方案:") 391 | this.schema_list := this.AddListView("Section Checked NoSort w220 h175", ["方案名称"]) 392 | this.schema_list.OnEvent("Click", (ctrl, lvid) => this.OnSchemaListClick(lvid)) 393 | this.schema_list.OnEvent("ItemCheck", (ctrl, lvid, checked) => this.OnSchemaListItemCheck(lvid, checked)) 394 | this.description := this.AddText("YP w285 h175", "选中列表中的输入方案以查看简介") 395 | this.AddText("XS", "在玉兔毫里,以下快捷键可唤出方案选单,以切换模式或选用其他输入方案。") 396 | this.hotkeys := this.AddEdit("-Multi ReadOnly r1 w505") 397 | this.proxy_prompt := this.AddText("XS", "代理服务器:") 398 | this.proxy := this.AddEdit("X+10 -Multi r1 w300") 399 | DllCall("SendMessage", "Ptr", this.proxy.Hwnd, "UInt", EM_SETCUEBANNER := 0x1501, "UPtr", true, "WStr", "如 http://127.0.0.1:7890", "Ptr") 400 | this.use_git := this.AddCheckbox("X+20", "使用 Git") 401 | this.use_git.Value := 1 402 | this.more_schemas := this.AddButton("XS w155", "获取更多输入方案…") 403 | this.more_schemas.OnEvent("Click", (*) => this.OnGetSchema()) 404 | this.ok := this.AddButton("X+60 YP w90", "中") 405 | this.ok.OnEvent("Click", (*) => this.OnOK()) 406 | 407 | this.Populate() 408 | } 409 | 410 | Populate() { 411 | if !this.settings 412 | return 413 | local available := this.api.get_available_schema_list(this.settings) 414 | local selected := this.api.get_selected_schema_list(this.settings) 415 | this.schema_list.Delete() 416 | 417 | local recruited := Map() 418 | 419 | local selected_list := selected.list 420 | local available_list := available.list 421 | Loop selected.size { 422 | local schema_id := selected_list[A_Index].schema_id 423 | Loop available.size { 424 | item := available_list[A_Index] 425 | info := RimeSchemaInfo(item) 426 | if item.schema_id == schema_id && (!recruited.Has(info.Ptr) || recruited[info.Ptr] == false) { 427 | recruited[info.Ptr] := true 428 | row := this.schema_list.Add("Check", item.name) 429 | this.item_data[row] := info 430 | break 431 | } 432 | } 433 | } 434 | Loop available.size { 435 | item := available_list[A_Index] 436 | info := RimeSchemaInfo(item) 437 | if !recruited.Has(info.Ptr) || recruited[info.Ptr] == false { 438 | recruited[info.Ptr] := true 439 | row := this.schema_list.Add(, item.name) 440 | this.item_data[row] := info 441 | } 442 | } 443 | txt := this.api.get_hotkeys(this.settings) 444 | this.hotkeys.Value := txt 445 | this.loaded := true 446 | this.modified := false 447 | } 448 | 449 | OnSchemaListClick(lvid) { 450 | if !this.loaded || !this.schema_list || lvid <= 0 || lvid > this.schema_list.GetCount() { 451 | return 452 | } 453 | this.ShowDetails(this.item_data[lvid]) 454 | } 455 | 456 | OnSchemaListItemCheck(lvid, checked) { 457 | if !this.loaded || !this.schema_list || lvid <= 0 || lvid > this.schema_list.GetCount() { 458 | return 459 | } 460 | this.modified := true 461 | } 462 | 463 | ShowDetails(info) { 464 | if !info 465 | return 466 | details := "" 467 | if name := this.api.get_schema_name(info) 468 | details .= name 469 | if author := this.api.get_schema_author(info) 470 | details .= "`r`n`r`n" . author 471 | if description := this.api.get_schema_description(info) 472 | details .= "`r`n`r`n" . description 473 | this.description.Value := details 474 | } 475 | 476 | OnOK() { 477 | if this.modified && !!this.settings && this.schema_list.GetCount() != 0 { 478 | selection := [] 479 | row := 0 480 | while row := this.schema_list.GetNext(row, "Checked") { 481 | if info := this.item_data[row] 482 | selection.Push(this.api.get_schema_id(info)) 483 | } 484 | if selection.Length == 0 { 485 | MsgBox("至少要选用一项吧。", "玉兔毫不是这般用法", "Icon!") 486 | return 487 | } 488 | this.api.select_schemas(this.settings, selection) 489 | } 490 | this.Exit(true) 491 | } 492 | 493 | OnGetSchema() { 494 | if !FileExist(Format("{}\rime-install.bat", A_ScriptDir)) { 495 | MsgBox("未找到东风破安装脚本,请检查安装目录。", ":-(", "Ok Iconx") 496 | return 497 | } 498 | 499 | if this.proxy.Value { 500 | EnvSet("http_proxy", this.proxy.Value) 501 | EnvSet("https_proxy", this.proxy.Value) 502 | } 503 | if this.use_git.Value { 504 | EnvSet("use_plum", "1") 505 | } else { 506 | EnvSet("use_plum", "0") 507 | } 508 | EnvSet("rime_dir", RabbitUserDataPath()) 509 | this.Opt("+Disabled") 510 | RunWait(Format("cmd.exe /k {}\rime-install.bat", A_ScriptDir), A_ScriptDir) 511 | this.Opt("-Disabled") 512 | WinActivate("ahk_id " this.Hwnd) 513 | this.api.load_settings(this.settings) 514 | this.Populate() 515 | } 516 | 517 | Exit(yes) { 518 | this.result.yes := yes 519 | this.Destroy() 520 | } 521 | } 522 | 523 | class UIStyleSettings { 524 | __New() { 525 | this.api := RimeLeversApi() 526 | this.settings := this.api.custom_settings_init("rabbit", "Rabbit.UIStyleSettings") 527 | } 528 | 529 | GetPresetColorSchemes() { 530 | global rime 531 | local result := [] 532 | if !config := this.api.settings_get_config(this.settings) 533 | return result 534 | if !rime || !preset := rime.config_begin_map(config, "preset_color_schemes") 535 | return result 536 | while rime.config_next(preset) { 537 | local name_key := preset.path . "/name" 538 | if !name := rime.config_get_cstring(config, name_key) 539 | continue 540 | local author_key := preset.path . "/author" 541 | local author := rime.config_get_cstring(config, author_key) 542 | UIStyle.UpdateColor(config, StrLower(preset.key)) 543 | result.Push({ 544 | color_scheme_id: preset.key, 545 | name: name, 546 | author: author, 547 | border_color: UIStyle.border_color, 548 | text_color: UIStyle.text_color, 549 | back_color: UIStyle.back_color, 550 | hilited_text_color: UIStyle.hilited_text_color, 551 | hilited_back_color: UIStyle.hilited_back_color, 552 | hilited_candidate_text_color: UIStyle.hilited_candidate_text_color, 553 | hilited_candidate_back_color: UIStyle.hilited_candidate_back_color, 554 | candidate_text_color: UIStyle.candidate_text_color, 555 | candidate_back_color: UIStyle.candidate_back_color, 556 | font_face: UIStyle.font_face, 557 | font_point: UIStyle.font_point, 558 | }) 559 | } 560 | return result 561 | } 562 | 563 | GetActiveColorScheme() { 564 | global rime 565 | if !config := this.api.settings_get_config(this.settings) 566 | return "" 567 | if !rime || !value := rime.config_get_cstring(config, "style/color_scheme") 568 | return "" 569 | return value 570 | } 571 | 572 | SelectColorScheme(color_scheme_id) { 573 | this.api.customize_string(this.settings, "style/color_scheme", color_scheme_id) 574 | return true 575 | } 576 | } 577 | 578 | class UIStyleSettingsDialog extends Gui { 579 | __New(settings, result) { 580 | super.__New("-MaximizeBox -MinimizeBox", "【玉兔毫】界面风格设定", this) 581 | this.settings := settings 582 | this.loaded := false 583 | this.api := RimeLeversApi() 584 | 585 | this.preset := [] 586 | this.result := result 587 | 588 | ; Layout 589 | this.MarginX := 15 590 | this.MarginY := 15 591 | this.color_schemes_width := 220 592 | this.preview_width := 220 593 | this.preview_offset := 20 594 | this.AddText("x10 y10", "主题:").GetPos(, , , &h) 595 | this.title_height := h 596 | this.color_schemes := this.AddListBox(Format("Section r15 w{} -Multi", this.color_schemes_width)) 597 | this.color_schemes.OnEvent("Change", (ctrl, info) => this.OnColorSchemeSelChange()) 598 | this.color_schemes.GetPos(, , , &h) 599 | this.list_height := h 600 | this.AddGroupBox(Format("x+{} yp-8 w{} h{}", this.preview_offset, this.preview_width, this.list_height + 8), "预览") 601 | ; 0xE(SS_BITMAP) or 0x4E (Bitmap and Resizable, but text is unclear) 602 | this.preview_img := this.AddPicture("xp+50 yp+50 w180 h180 0xE BackgroundWhite") 603 | this.candidate_box := CandidatePreview(this.preview_img) 604 | 605 | this.set_font := this.AddButton(Format("xs ys+{} w120", this.list_height + this.MarginY), "设置字体") 606 | this.set_font.Opt("+Disabled") ; TODO: implement font setting 607 | this.ok := this.AddButton("x+180 w90", "中") 608 | this.ok.OnEvent("Click", (*) => this.OnOK()) 609 | 610 | this.Populate() 611 | } 612 | 613 | Populate() { 614 | if !this.settings 615 | return 616 | local active := this.settings.GetActiveColorScheme() 617 | local active_index := 0 618 | this.preset := this.settings.GetPresetColorSchemes() 619 | local names := [] 620 | for i, info in this.preset { 621 | names.Push(info.name) 622 | if info.color_scheme_id = active 623 | active_index := i 624 | } 625 | this.color_schemes.Opt("-Redraw") 626 | this.color_schemes.Add(names) 627 | this.color_schemes.Opt("+Redraw") 628 | if active_index > 0 { 629 | this.color_schemes.Choose(active_index) 630 | this.Preview(active_index) 631 | } 632 | this.loaded := true 633 | } 634 | 635 | OnColorSchemeSelChange() { 636 | local index := this.color_schemes.Value 637 | if index > 0 && index <= this.preset.Length { 638 | this.settings.SelectColorScheme(this.preset[index].color_scheme_id) 639 | this.Preview(index) 640 | } 641 | return 0 642 | } 643 | 644 | Preview(index) { 645 | if index <= 0 || index > this.preset.Length 646 | return 647 | local info := this.preset[index] 648 | this.candidate_box.Build(info, &box_width, &box_height) 649 | box_width := box_width / this.candidate_box.dpiScale 650 | box_height := box_height / this.candidate_box.dpiScale 651 | local box_x := this.MarginX + this.color_schemes_width + this.preview_offset + Round((this.preview_width - box_width) / 2) 652 | local box_y := this.MarginY + this.title_height + 8 + Round((this.list_height - box_height) / 2) 653 | this.preview_img.Move(box_x, box_y, box_width, box_height) 654 | this.candidate_box.Render(["输入法", "输入", "数", "书", "输"], 1) 655 | } 656 | 657 | OnOK() { 658 | this.Exit(true) 659 | } 660 | 661 | Exit(yes) { 662 | this.result.yes := yes 663 | this.Destroy() 664 | } 665 | } 666 | -------------------------------------------------------------------------------- /Lib/RabbitCandidateBox.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 - 2025 Xuesong Peng 3 | * Copyright (c) 2005 Tim 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | */ 19 | 20 | #Include 21 | #Include 22 | 23 | ; https://learn.microsoft.com/windows/win32/winmsg/extended-window-styles 24 | global WS_EX_NOACTIVATE := "+E0x8000000" 25 | global WS_EX_COMPOSITED := "+E0x02000000" 26 | global WS_EX_LAYERED := "+E0x00080000" 27 | 28 | class CandidateBox { 29 | gui := 0 30 | static isHidden := 1 31 | 32 | __New() { 33 | ; +E0x8080088: WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST 34 | this.gui := Gui("-Caption -DPIScale +E0x8080088") 35 | 36 | this.d2d := Direct2D(this.gui.Hwnd) 37 | this.dpiScale := this.d2d.GetDesktopDpiScale() 38 | 39 | this.UpdateUIStyle() 40 | } 41 | 42 | __Delete() { 43 | this.Hide() 44 | if this.gui 45 | this.gui.Destroy() 46 | } 47 | 48 | UpdateUIStyle() { 49 | this.borderWidth := UIStyle.border_width 50 | this.borderColor := UIStyle.border_color 51 | this.boxCornerR := UIStyle.corner_radius 52 | this.hlCornerR := UIStyle.round_corner 53 | this.lineSpacing := UIStyle.margin_y 54 | this.padding := UIStyle.margin_x 55 | 56 | this.mainFont := this.CreateFontObj(UIStyle.font_face, UIStyle.font_point) 57 | this.labFont := this.CreateFontObj(UIStyle.label_font_face, UIStyle.label_font_point) 58 | this.commentFont := this.CreateFontObj(UIStyle.comment_font_face, UIStyle.comment_font_point) 59 | 60 | ; preedite style 61 | this.textColor := UIStyle.text_color 62 | this.backgroundColor := UIStyle.back_color 63 | this.hlTxtColor := UIStyle.hilited_text_color 64 | this.hlBgColor := UIStyle.hilited_back_color 65 | ; candidate style 66 | this.hlCandTxtColor := UIStyle.hilited_candidate_text_color 67 | this.hlCandBgColor := UIStyle.hilited_candidate_back_color 68 | this.candTxtColor := UIStyle.candidate_text_color 69 | this.candBgColor := UIStyle.candidate_back_color 70 | 71 | ; some color schemes have no these colors 72 | this.labelColor := UIStyle.label_color 73 | this.hlLabelColor := UIStyle.hilited_label_color 74 | this.commentTxtColor := UIStyle.comment_text_color 75 | this.hlCommentTxtColor := UIStyle.hilited_comment_text_color 76 | } 77 | 78 | CreateFontObj(name, size) { 79 | local em2pt := 96.0 / 72.0 80 | local px := size * em2pt * this.dpiScale 81 | return { name: name, size: px } 82 | } 83 | 84 | Build(context, &winW, &winH) { ; build text layout 85 | local menu := context.menu 86 | local cands := menu.candidates 87 | this.num_candidates := menu.num_candidates 88 | this.hilited_index := menu.highlighted_candidate_index + 1 89 | 90 | GetCompositionText(context.composition, &pre_selected, &selected, &post_selected) 91 | 92 | ; Build preedit layout 93 | baseX := this.borderWidth + this.padding 94 | baseY := this.borderWidth + this.lineSpacing 95 | prd0 := this.GetTextMetrics(pre_selected, this.mainFont) 96 | prd1 := this.GetTextMetrics(selected, this.mainFont) 97 | prd2 := this.GetTextMetrics(post_selected, this.mainFont) 98 | prd1X := baseX + prd0.w + this.padding 99 | prd2X := prd1X + prd1.w 100 | this.preeditLayout := { 101 | selBox: { x: baseX, y: baseY, w: prd0.w, h: prd0.h, text: pre_selected }, 102 | hlSelBox: { x: prd1X, y: baseY, w: prd1.w, h: prd1.h, text: selected }, 103 | hlUnSelBox: { x: prd2X, y: baseY, w: prd2.w, h: prd2.h, text: post_selected }, 104 | left: baseX, 105 | top: baseY, 106 | width: prd0.w + this.padding + prd1.w + prd2.w, 107 | height: Max(prd0.h, prd1.h, prd2.h) 108 | } 109 | maxRowWidth := this.preeditLayout.width 110 | 111 | ; Build candidates layout 112 | totalRowsHeight := this.preeditLayout.height + this.lineSpacing 113 | baseY := baseY + totalRowsHeight 114 | this.candidatesLayout := { labels: [], cands: [], comments: [], rows: [] } 115 | 116 | has_label := !!context.select_labels[0] 117 | select_keys := menu.select_keys 118 | num_select_keys := StrLen(select_keys) 119 | Loop this.num_candidates { 120 | labelText := String(A_Index) 121 | if A_Index <= menu.page_size && has_label 122 | labelText := context.select_labels[A_Index] || labelText 123 | else if A_Index <= num_select_keys 124 | labelText := SubStr(select_keys, A_Index, 1) 125 | labelText := Format(UIStyle.label_format, labelText) 126 | labelBox := this.GetTextMetrics(labelText, this.labFont) 127 | this.candidatesLayout.labels.Push({ x: baseX, y: baseY, w: labelBox.w, h: labelBox.h, text: labelText }) 128 | 129 | candText := cands[A_Index].text 130 | candBox := this.GetTextMetrics(candText, this.mainFont) 131 | this.candidatesLayout.cands.Push({ x: baseX + labelBox.w + this.padding, y: baseY, w: candBox.w, h: candBox.h, text: candText }) 132 | 133 | commentText := cands[A_Index].comment 134 | commentBox := this.GetTextMetrics(commentText, this.commentFont) 135 | this.candidatesLayout.comments.Push({ x: baseX + labelBox.w + candBox.w, y: baseY, w: commentBox.w, h: commentBox.h, text: commentText }) 136 | 137 | rowRect := { 138 | x: baseX, y: baseY, 139 | w: labelBox.w + this.padding + candBox.w + (commentText ? this.padding * 2 + commentBox.w : 0), 140 | h: Max(labelBox.h, candBox.h, commentBox.h) 141 | } 142 | this.candidatesLayout.rows.Push(rowRect) 143 | if (rowRect.w > maxRowWidth) { 144 | maxRowWidth := rowRect.w 145 | } 146 | increment := rowRect.h + this.lineSpacing 147 | baseY += increment, totalRowsHeight += increment 148 | } 149 | totalRowsHeight -= this.lineSpacing ; remove extra line spacing 150 | 151 | this.commentOffset := 0 152 | this.boxWidth := Ceil(maxRowWidth) + (this.borderWidth + this.padding) * 2 153 | if this.boxWidth < UIStyle.min_width { 154 | this.commentOffset := UIStyle.min_width - this.boxWidth 155 | this.boxWidth := UIStyle.min_width 156 | } 157 | this.boxHeight := Ceil(totalRowsHeight) + (this.borderWidth + this.padding) * 2 158 | winW := this.boxWidth 159 | winH := this.boxHeight 160 | 161 | ; get better spacing to align comments 162 | loop this.num_candidates { 163 | labelW := this.candidatesLayout.labels[A_Index].w 164 | candW := this.candidatesLayout.cands[A_Index].w 165 | comment := this.candidatesLayout.comments[A_Index] 166 | 167 | if comment.w > 0 { 168 | alignCommentGap := maxRowWidth - labelW - candW - comment.w - this.padding 169 | comment.x += alignCommentGap + this.commentOffset 170 | } 171 | } 172 | } 173 | 174 | Show(x, y) { 175 | if (CandidateBox.isHidden) { 176 | this.gui.Show("NA") 177 | CandidateBox.isHidden := 0 178 | } 179 | 180 | this.d2d.SetPosition(x, y, this.boxWidth, this.boxHeight) 181 | this.d2d.BeginDraw() 182 | 183 | if (this.borderWidth > 0) { 184 | ; Draw outer border as filled rounded rectangle (border color) 185 | this.d2d.FillRoundedRectangle(0, 0, this.boxWidth, this.boxHeight, this.boxCornerR, this.boxCornerR, this.borderColor) 186 | ; Draw inner background next 187 | bgX := this.borderWidth, bgY := this.borderWidth 188 | bgW := this.boxWidth - this.borderWidth * 2 189 | bgH := this.boxHeight - this.borderWidth * 2 190 | bgR := this.boxCornerR > this.borderWidth ? this.boxCornerR - this.borderWidth : 0 191 | this.d2d.FillRoundedRectangle(bgX, bgY, bgW, bgH, bgR, bgR, this.backgroundColor) 192 | } else { 193 | this.d2d.FillRoundedRectangle(0, 0, this.boxWidth, this.boxHeight, this.boxCornerR, this.boxCornerR, this.backgroundColor) 194 | } 195 | 196 | ; Draw preedit 197 | if (this.preeditLayout.hlSelBox.text) { 198 | ; highlight background for preedit selection 199 | this.d2d.FillRoundedRectangle( 200 | this.preeditLayout.hlSelBox.x, this.preeditLayout.hlSelBox.y, 201 | this.preeditLayout.hlSelBox.w, this.preeditLayout.hlSelBox.h, 202 | this.hlCornerR, this.hlCornerR, this.hlBgColor) 203 | } 204 | this.d2d.DrawText(this.preeditLayout.selBox.text, this.preeditLayout.selBox.x, this.preeditLayout.selBox.y, this.mainFont.size, this.textColor, this.mainFont.name) 205 | this.d2d.DrawText(this.preeditLayout.hlSelBox.text, this.preeditLayout.hlSelBox.x, this.preeditLayout.hlSelBox.y, this.mainFont.size, this.hlTxtColor, this.mainFont.name) 206 | this.d2d.DrawText(this.preeditLayout.hlUnSelBox.text, this.preeditLayout.hlUnSelBox.x, this.preeditLayout.hlUnSelBox.y, this.mainFont.size, this.textColor, this.mainFont.name) 207 | 208 | hiliteW := this.boxWidth - this.borderWidth * 2 - this.padding * 2 209 | ; Draw candidates 210 | Loop this.num_candidates { 211 | rowRect := this.candidatesLayout.rows[A_Index] 212 | labelFg := this.labelColor 213 | candFg := this.candTxtColor 214 | commentFg := this.commentTxtColor 215 | if (A_Index == this.hilited_index) { ; Draw highlight if selected 216 | labelFg := this.hlLabelColor 217 | candFg := this.hlCandTxtColor 218 | commentFg := this.hlCommentTxtColor 219 | this.d2d.FillRoundedRectangle(rowRect.x, rowRect.y, hiliteW, rowRect.h, this.hlCornerR, this.hlCornerR, this.hlCandBgColor) 220 | } 221 | 222 | label := this.candidatesLayout.labels[A_Index] 223 | this.d2d.DrawText(label.text, label.x, label.y, this.labFont.size, labelFg, this.labFont.name) 224 | 225 | cand := this.candidatesLayout.cands[A_Index] 226 | this.d2d.DrawText(cand.text, cand.x, cand.y, this.mainFont.size, candFg, this.mainFont.name) 227 | 228 | comment := this.candidatesLayout.comments[A_Index] 229 | if comment.w > 0 { 230 | this.d2d.DrawText(comment.text, comment.x, comment.y, this.commentFont.size, commentFg, this.commentFont.name) 231 | } 232 | } 233 | 234 | this.d2d.EndDraw() 235 | } 236 | 237 | Hide() { 238 | if (!CandidateBox.isHidden) { 239 | this.d2d.EndDraw() 240 | this.d2d.Clear() 241 | this.gui.Hide() 242 | CandidateBox.isHidden := 1 243 | } 244 | } 245 | 246 | GetTextMetrics(text, fontObj) { 247 | if !text 248 | return { w: 0, h: 0 } 249 | 250 | return this.d2d.GetMetrics(text, fontObj.name, fontObj.size) 251 | } 252 | } 253 | 254 | class LegacyCandidateBox { 255 | static dbg := false 256 | static gui := 0 257 | static border := LegacyCandidateBox.dbg ? "+border" : 0 258 | 259 | __New() { 260 | this.UpdateUIStyle() 261 | } 262 | 263 | UpdateUIStyle() { 264 | ; alpha not supported 265 | del_opaque(color) { 266 | return color & 0xffffff 267 | } 268 | LegacyCandidateBox.text_color := del_opaque(UIStyle.text_color) 269 | LegacyCandidateBox.back_color := del_opaque(UIStyle.back_color) 270 | LegacyCandidateBox.candidate_text_color := del_opaque(UIStyle.candidate_text_color) 271 | LegacyCandidateBox.candidate_back_color := del_opaque(UIStyle.candidate_back_color) 272 | LegacyCandidateBox.label_color := del_opaque(UIStyle.label_color) 273 | LegacyCandidateBox.comment_text_color := del_opaque(UIStyle.comment_text_color) 274 | LegacyCandidateBox.hilited_text_color := del_opaque(UIStyle.hilited_text_color) 275 | LegacyCandidateBox.hilited_back_color := del_opaque(UIStyle.hilited_back_color) 276 | LegacyCandidateBox.hilited_candidate_text_color := del_opaque(UIStyle.hilited_candidate_text_color) 277 | LegacyCandidateBox.hilited_candidate_back_color := del_opaque(UIStyle.hilited_candidate_back_color) 278 | LegacyCandidateBox.hilited_label_color := del_opaque(UIStyle.hilited_label_color) 279 | LegacyCandidateBox.hilited_comment_text_color := del_opaque(UIStyle.hilited_comment_text_color) 280 | 281 | LegacyCandidateBox.base_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.text_color, LegacyCandidateBox.back_color, LegacyCandidateBox.border) 282 | LegacyCandidateBox.candidate_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.candidate_text_color, LegacyCandidateBox.candidate_back_color, LegacyCandidateBox.border) 283 | LegacyCandidateBox.label_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.label_color, LegacyCandidateBox.candidate_back_color, LegacyCandidateBox.border) 284 | LegacyCandidateBox.comment_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.comment_text_color, LegacyCandidateBox.candidate_back_color, LegacyCandidateBox.border) 285 | LegacyCandidateBox.hilited_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.hilited_text_color, LegacyCandidateBox.hilited_back_color, LegacyCandidateBox.border) 286 | LegacyCandidateBox.hilited_candidate_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.hilited_candidate_text_color, LegacyCandidateBox.hilited_candidate_back_color, LegacyCandidateBox.border) 287 | LegacyCandidateBox.hilited_label_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.hilited_label_color, LegacyCandidateBox.hilited_candidate_back_color, LegacyCandidateBox.border) 288 | LegacyCandidateBox.hilited_comment_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.hilited_comment_text_color, LegacyCandidateBox.hilited_candidate_back_color, LegacyCandidateBox.border) 289 | 290 | LegacyCandidateBox.base_font_opt := Format("s{} q5", UIStyle.font_point) 291 | LegacyCandidateBox.label_font_opt := Format("s{} q5", UIStyle.label_font_point) 292 | LegacyCandidateBox.comment_font_opt := Format("s{} q5", UIStyle.comment_font_point) 293 | 294 | if LegacyCandidateBox.gui { 295 | LegacyCandidateBox.gui.BackColor := LegacyCandidateBox.back_color 296 | LegacyCandidateBox.gui.MarginX := UIStyle.margin_x 297 | LegacyCandidateBox.gui.MarginY := UIStyle.margin_y 298 | 299 | if HasProp(LegacyCandidateBox.gui, "pre") && LegacyCandidateBox.gui.pre 300 | LegacyCandidateBox.gui.pre.Opt(LegacyCandidateBox.base_opt) 301 | if HasProp(LegacyCandidateBox.gui, "sel") && LegacyCandidateBox.gui.sel 302 | LegacyCandidateBox.gui.sel.Opt(LegacyCandidateBox.hilited_opt) 303 | if HasProp(LegacyCandidateBox.gui, "post") && LegacyCandidateBox.gui.post 304 | LegacyCandidateBox.gui.post.Opt(LegacyCandidateBox.base_opt) 305 | } 306 | } 307 | 308 | Build(context, &width, &height) { 309 | if !LegacyCandidateBox.gui || !LegacyCandidateBox.gui.built 310 | LegacyCandidateBox.gui := LegacyCandidateBox.BoxGui(context) 311 | else 312 | LegacyCandidateBox.gui.Update(context) 313 | width := LegacyCandidateBox.gui.max_width 314 | height := LegacyCandidateBox.gui.max_height 315 | } 316 | 317 | Show(x, y) { 318 | LegacyCandidateBox.gui.Show(Format("AutoSize NA x{} y{}", x, y)) 319 | } 320 | 321 | Hide() { 322 | if LegacyCandidateBox.gui && HasMethod(LegacyCandidateBox.gui, "Show") 323 | LegacyCandidateBox.gui.Show("Hide") 324 | } 325 | 326 | class BoxGui extends Gui { 327 | built := false 328 | __New(context, &pre?, &sel?, &post?, &menu?) { 329 | super.__New(, , this) 330 | 331 | menu := context.menu 332 | local cands := menu.candidates 333 | local num_candidates := menu.num_candidates 334 | local hilited_index := menu.highlighted_candidate_index + 1 335 | local composition := context.composition 336 | GetCompositionText(composition, &pre, &sel, &post) 337 | 338 | this.Opt(Format("-DPIScale -Caption +Owner +AlwaysOnTop {} {} {}", WS_EX_NOACTIVATE, WS_EX_COMPOSITED, WS_EX_LAYERED)) 339 | this.BackColor := LegacyCandidateBox.back_color 340 | this.SetFont(LegacyCandidateBox.base_font_opt, UIStyle.font_face) 341 | this.MarginX := UIStyle.margin_x 342 | this.MarginY := UIStyle.margin_y 343 | this.num_candidates := num_candidates 344 | this.has_comment := false 345 | 346 | ; build preedit 347 | this.max_width := 0 348 | this.preedit_height := 0 349 | local head_position := Format("x{} y{} section {}", this.MarginX, this.MarginY, LegacyCandidateBox.border) 350 | local position := head_position 351 | if pre { 352 | this.pre := this.AddText(position, pre) 353 | this.pre.Opt(LegacyCandidateBox.base_opt) 354 | position := Format("x+{} ys {}", this.MarginX, LegacyCandidateBox.border) 355 | this.pre.GetPos(, , &w, &h) 356 | this.preedit_height := max(this.preedit_height, h) 357 | this.pre_width := w 358 | this.max_width += (w + this.MarginX) 359 | } 360 | if sel { 361 | this.sel := this.AddText(position, sel) 362 | this.sel.Opt(LegacyCandidateBox.hilited_opt) 363 | position := Format("x+{} ys {}", this.MarginX, LegacyCandidateBox.border) 364 | this.sel.GetPos(, , &w, &h) 365 | this.preedit_height := max(this.preedit_height, h) 366 | this.sel_width := w 367 | this.max_width += (w + this.MarginX) 368 | } 369 | if post { 370 | this.post := this.AddText(position, post) 371 | this.post.Opt(LegacyCandidateBox.base_opt) 372 | this.post.GetPos(, , &w, &h) 373 | this.preedit_height := max(this.preedit_height, h) 374 | this.post_width := w 375 | this.max_width += w 376 | } 377 | 378 | ; build candidates 379 | this.max_label_width := 0 380 | this.max_candidate_width := 0 381 | this.max_comment_width := 0 382 | this.candidate_height := 0 383 | local has_label := !!context.select_labels[0] 384 | local select_keys := menu.select_keys 385 | local num_select_keys := StrLen(select_keys) 386 | loop num_candidates { 387 | position := Format("xs y+{} section {}", this.MarginY, LegacyCandidateBox.border) 388 | local label_text := String(A_Index) 389 | if A_Index <= menu.page_size && has_label 390 | label_text := context.select_labels[A_Index] 391 | else if A_Index <= num_select_keys 392 | label_text := SubStr(select_keys, A_Index, 1) 393 | label_text := Format(UIStyle.label_format, label_text) 394 | this.SetFont(LegacyCandidateBox.label_font_opt, UIStyle.label_font_face) 395 | local label := this.AddText(Format("Right {} vL{}", position, A_Index), label_text) 396 | label.GetPos(, , &w, &h1) 397 | this.max_label_width := max(this.max_label_width, w + this.MarginX) 398 | 399 | position := Format("x+{} ys {}", this.MarginX, LegacyCandidateBox.border) 400 | this.SetFont(LegacyCandidateBox.base_font_opt, UIStyle.font_face) 401 | local candidate := this.AddText(Format("{} vC{}", position, A_Index), cands[A_Index].text) 402 | candidate.GetPos(, , &w, &h2) 403 | this.max_candidate_width := max(this.max_candidate_width, w + this.MarginX) 404 | 405 | if comment_text := cands[A_Index].comment 406 | this.has_comment := true 407 | this.SetFont(LegacyCandidateBox.comment_font_opt, UIStyle.comment_font_face) 408 | local comment := this.AddText(Format("{} vM{}", position, A_Index), comment_text) 409 | comment.GetPos(, , &w, &h3) 410 | comment.Opt(Format("c{:x}", LegacyCandidateBox.comment_text_color)) 411 | comment.Visible := this.has_comment 412 | this.max_comment_width := max(this.max_comment_width, w) 413 | this.candidate_height := max(this.candidate_height, h1, h2, h3) 414 | 415 | if A_Index == hilited_index { 416 | label.Opt(LegacyCandidateBox.hilited_label_opt) 417 | candidate.Opt(LegacyCandidateBox.hilited_candidate_opt) 418 | comment.Opt(LegacyCandidateBox.hilited_comment_opt) 419 | } else { 420 | label.Opt(LegacyCandidateBox.label_opt) 421 | candidate.Opt(LegacyCandidateBox.candidate_opt) 422 | comment.Opt(LegacyCandidateBox.comment_opt) 423 | } 424 | } 425 | 426 | ; adjust width height 427 | local list_width := this.max_label_width + this.max_candidate_width + this.has_comment * this.max_comment_width 428 | local box_width := max(UIStyle.min_width, list_width) 429 | if box_width > this.max_width && HasProp(this, "post") && this.post 430 | this.post.Move(, , this.post_width + box_width - this.max_width) 431 | this.max_width := max(box_width, this.max_width) 432 | if this.max_width > list_width { 433 | this.max_candidate_width += this.max_width - list_width 434 | loop num_candidates 435 | this["C" . A_Index].Move(, , this.max_candidate_width) 436 | } 437 | local y := 2 * this.MarginY + this.preedit_height 438 | loop num_candidates { 439 | local x := this.MarginX 440 | this["L" . A_Index].Move(x, y, this.max_label_width) 441 | this["L" . A_Index].GetPos(, , , &h) 442 | local max_h := h 443 | x += this.max_label_width 444 | this["C" . A_Index].Move(x, y, this.max_candidate_width) 445 | this["C" . A_Index].GetPos(, , , &h) 446 | max_h := max(max_h, h) 447 | x += this.max_candidate_width 448 | this["M" . A_Index].Move(x, y, this.max_comment_width) 449 | this["M" . A_Index].GetPos(, , , &h) 450 | max_h := max(max_h, h) 451 | y += (max_h + this.MarginY) 452 | } 453 | this.max_height := y 454 | this.max_width += (2 * this.MarginX) 455 | 456 | this.built := true 457 | } 458 | 459 | Update(context) { 460 | local fake_gui := LegacyCandidateBox.BoxGui(context, &pre, &sel, &post, &menu) 461 | local num_candidates := menu.num_candidates 462 | local hilited_index := menu.highlighted_candidate_index + 1 463 | this.SetFont(LegacyCandidateBox.base_font_opt, UIStyle.font_face) 464 | this.num_candidates := max(this.num_candidates, num_candidates) 465 | this.max_width := fake_gui.max_width 466 | this.max_height := fake_gui.max_height 467 | 468 | ; reset preedit 469 | if pre { 470 | if !HasProp(this, "pre") || !this.pre 471 | this.pre := this.AddText(, pre) 472 | this.pre.Value := fake_gui.pre.Value 473 | fake_gui.pre.GetPos(&x, &y, &w, &h) 474 | this.pre.Move(x, y, w, h) 475 | } 476 | if HasProp(this, "pre") && this.pre 477 | this.pre.Visible := !!pre 478 | if sel { 479 | if !HasProp(this, "sel") || !this.sel 480 | this.sel := this.AddText(, sel) 481 | this.sel.Value := fake_gui.sel.Value 482 | fake_gui.sel.GetPos(&x, &y, &w, &h) 483 | this.sel.Move(x, y, w, h) 484 | } 485 | if HasProp(this, "sel") && this.sel 486 | this.sel.Visible := !!sel 487 | if post { 488 | if !HasProp(this, "post") || !this.post 489 | this.post := this.AddText(, post) 490 | this.post.Value := fake_gui.post.Value 491 | fake_gui.post.GetPos(&x, &y, &w, &h) 492 | this.post.Move(x, y, w, h) 493 | } 494 | if HasProp(this, "post") && this.post 495 | this.post.Visible := !!post 496 | 497 | ; reset candidates 498 | loop this.num_candidates { 499 | if A_Index > num_candidates { 500 | this["L" . A_Index].Visible := false 501 | this["C" . A_Index].Visible := false 502 | this["M" . A_Index].Visible := false 503 | continue 504 | } 505 | local fake_label := fake_gui["L" . A_Index] 506 | local fake_candidate := fake_gui["C" . A_Index] 507 | local fake_comment := fake_gui["M" . A_Index] 508 | this.SetFont(LegacyCandidateBox.label_font_opt, UIStyle.label_font_face) 509 | try 510 | local label := this["L" . A_Index] 511 | catch 512 | local label := this.AddText(Format("vL{}", A_Index), fake_label.Value) 513 | this.SetFont(LegacyCandidateBox.base_font_opt, UIStyle.font_face) 514 | try 515 | local candidate := this["C" . A_Index] 516 | catch 517 | local candidate := this.AddText(Format("vC{}", A_Index), fake_candidate.Value) 518 | this.SetFont(LegacyCandidateBox.comment_font_opt, UIStyle.comment_font_face) 519 | try 520 | local comment := this["M" . A_Index] 521 | catch 522 | local comment := this.AddText(Format("vM{}", A_Index), fake_comment.Value) 523 | label.Value := fake_label.Value 524 | fake_label.GetPos(&x, &y, &w, &h) 525 | label.Move(x, y, w, h) 526 | candidate.Value := fake_candidate.Value 527 | fake_candidate.GetPos(&x, &y, &w, &h) 528 | candidate.Move(x, y, w, h) 529 | comment.Value := fake_comment.Value 530 | fake_comment.GetPos(&x, &y, &w, &h) 531 | comment.Move(x, y, w, h) 532 | 533 | if A_Index == hilited_index { 534 | label.Opt(LegacyCandidateBox.hilited_label_opt) 535 | candidate.Opt(LegacyCandidateBox.hilited_candidate_opt) 536 | comment.Opt(LegacyCandidateBox.hilited_comment_opt) 537 | } else { 538 | label.Opt(LegacyCandidateBox.label_opt) 539 | candidate.Opt(LegacyCandidateBox.candidate_opt) 540 | comment.Opt(LegacyCandidateBox.comment_opt) 541 | } 542 | local visible := (A_Index <= num_candidates) 543 | label.Visible := visible 544 | candidate.Visible := visible 545 | comment.Visible := (fake_gui.has_comment && visible) 546 | } 547 | 548 | fake_gui.GetPos(, , &width, &height) 549 | this.Move(, , width, height) 550 | } 551 | } 552 | } 553 | 554 | GetCompositionText(composition, &pre_selected, &selected, &post_selected) { 555 | pre_selected := "" 556 | selected := "" 557 | post_selected := "" 558 | if not preedit := composition.preedit 559 | return false 560 | 561 | static cursor_text := "‸" ; or 𝙸 562 | static cursor_size := StrPut(cursor_text, "UTF-8") - 1 ; do not count tailing null 563 | 564 | local preedit_length := StrPut(preedit, "UTF-8") 565 | local selected_start := composition.sel_start 566 | local selected_end := composition.sel_end 567 | 568 | local preedit_buffer ; insert caret text into preedit text if applicable 569 | if 0 <= composition.cursor_pos and composition.cursor_pos <= preedit_length { 570 | preedit_buffer := Buffer(preedit_length + cursor_size, 0) 571 | local temp_preedit := Buffer(preedit_length, 0) 572 | StrPut(preedit, temp_preedit, "UTF-8") 573 | local src := temp_preedit.Ptr 574 | local tgt := preedit_buffer.Ptr 575 | Loop composition.cursor_pos { 576 | byte := NumGet(src, A_Index - 1, "Char") 577 | NumPut("Char", byte, tgt, A_Index - 1) 578 | } 579 | src := src + composition.cursor_pos 580 | tgt := tgt + composition.cursor_pos 581 | StrPut(cursor_text, tgt, "UTF-8") 582 | tgt := tgt + cursor_size 583 | Loop preedit_length - composition.cursor_pos { 584 | byte := NumGet(src, A_Index - 1, "Char") 585 | NumPut("Char", byte, tgt, A_Index - 1) 586 | } 587 | preedit_length := preedit_length + cursor_size 588 | if selected_start >= composition.cursor_pos 589 | selected_start := selected_start + cursor_size 590 | if selected_end > composition.cursor_pos 591 | selected_end := selected_end + cursor_size 592 | } else { 593 | preedit_buffer := Buffer(preedit_length, 0) 594 | StrPut(preedit, preedit_buffer, "UTF-8") 595 | } 596 | 597 | if 0 <= selected_start and selected_start < selected_end and selected_end <= preedit_length { 598 | pre_selected := StrGet(preedit_buffer, selected_start, "UTF-8") 599 | selected := StrGet(preedit_buffer.Ptr + selected_start, selected_end - selected_start, "UTF-8") 600 | post_selected := StrGet(preedit_buffer.Ptr + selected_end, "UTF-8") 601 | return true 602 | } else { 603 | pre_selected := StrGet(preedit_buffer, "UTF-8") 604 | return false 605 | } 606 | } 607 | --------------------------------------------------------------------------------