├── .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 |
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 |
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 | [](https://github.com/rimeinn/rabbit/releases/latest)
6 | [](https://github.com/rimeinn/rabbit/actions/workflows/ci.yaml)
7 | [](https://t.me/rime_rabbit)
8 | [](LICENSE)
9 | [](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 |
--------------------------------------------------------------------------------