diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dc687ae..67dbf9b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -155,6 +155,7 @@ jobs: with: name: Rabbit-${{ matrix.target }} path: | + Lib/Gdip/Gdip_All.ahk Lib/librime-ahk/*.ahk Lib/librime-ahk/rime.dll Lib/librime-ahk/utils @@ -175,6 +176,7 @@ jobs: name: Rabbit-Full-${{ matrix.target }} path: | Data + Lib/Gdip/Gdip_All.ahk Lib/librime-ahk/*.ahk Lib/librime-ahk/rime.dll Lib/librime-ahk/utils @@ -189,6 +191,48 @@ jobs: README.md rime-install.bat + create-nightly: + name: Create Nightly release + if: ${{ github.ref == 'refs/heads/master' }} + runs-on: ubuntu-latest + needs: build-rabbit + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download x64 + uses: actions/download-artifact@v4 + with: + name: Rabbit-Full-x64 + path: x64 + + - name: Pack x64 + working-directory: x64 + run: | + mkdir Rime && zip -r -q ../rabbit-nightly-x64.zip * + + - name: Download x86 + uses: actions/download-artifact@v4 + with: + name: Rabbit-Full-x86 + path: x86 + + - name: Pack x86 + working-directory: x86 + run: | + mkdir Rime && zip -r -q ../rabbit-nightly-x86.zip * + + - name: Upload Nightly + uses: marvinpinto/action-automatic-releases@latest + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + automatic_release_tag: latest + prerelease: true + title: "Nightly" + files: | + rabbit-nightly-x64.zip + rabbit-nightly-x86.zip + create-release: name: Create Release if: startsWith(github.ref, 'refs/tags/v') diff --git a/.gitignore b/.gitignore index 4ba4a2d..b58767d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ Rime Data +dist test.ahk *.exe *.dll diff --git a/.gitmodules b/.gitmodules index 37cdce0..48fc516 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "plum"] path = plum url = https://github.com/rime/plum +[submodule "Lib/Gdip"] + path = Lib/Gdip + url = https://github.com/buliasz/AHKv2-Gdip diff --git a/Lib/Gdip b/Lib/Gdip new file mode 160000 index 0000000..9fa1817 --- /dev/null +++ b/Lib/Gdip @@ -0,0 +1 @@ +Subproject commit 9fa18174be46326bc640dbfb542ac2c5d9399f44 diff --git a/Lib/GetCaretPosEx/GetCaretPosEx.patch b/Lib/GetCaretPosEx/GetCaretPosEx.patch index c412bee..331e1af 100644 --- a/Lib/GetCaretPosEx/GetCaretPosEx.patch +++ b/Lib/GetCaretPosEx/GetCaretPosEx.patch @@ -1,8 +1,8 @@ diff --git a/Lib/GetCaretPosEx/GetCaretPosEx.ahk b/Lib/GetCaretPosEx/GetCaretPosEx.ahk -index ff9a7f7..92f5109 100644 +index ff9a7f7..ac6cc79 100644 --- a/Lib/GetCaretPosEx/GetCaretPosEx.ahk +++ b/Lib/GetCaretPosEx/GetCaretPosEx.ahk -@@ -14,6 +14,11 @@ GetCaretPosEx(&left?, &top?, &right?, &bottom?, useHook := false) { +@@ -14,10 +14,17 @@ GetCaretPosEx(&left?, &top?, &right?, &bottom?, useHook := false) { className := WinGetClass(hwnd) catch className := "" @@ -14,7 +14,13 @@ index ff9a7f7..92f5109 100644 if className ~= "^(?:Windows|Microsoft)\.UI\..+" funcs := [getCaretPosFromUIA, getCaretPosFromHook, getCaretPosFromMSAA] 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}\]" -@@ -332,8 +332,12 @@ end: + funcs := [getCaretPosFromHook, getCaretPosFromWpfCaret] ++ else if className ~= "^Chrome_WidgetWin_.+" ++ funcs := [getCaretPosFromUIA, getCaretPosFromHook] + else + funcs := [getCaretPosFromMSAA, getCaretPosFromUIA, getCaretPosFromHook] + for fn in funcs { +@@ -332,8 +339,12 @@ end: } static getWindowScale(hwnd) { diff --git a/Lib/RabbitCandidateBox.ahk b/Lib/RabbitCandidateBox.ahk index 32140ca..5cb8af3 100644 --- a/Lib/RabbitCandidateBox.ahk +++ b/Lib/RabbitCandidateBox.ahk @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 - 2025 Xuesong Peng + * Copyright (c) 2005 Tim * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,112 +17,803 @@ * */ -global LVM_GETCOLUMNWIDTH := 0x101D +#Include +#Include +#Include + ; https://learn.microsoft.com/windows/win32/winmsg/extended-window-styles global WS_EX_NOACTIVATE := "+E0x8000000" +global WS_EX_COMPOSITED := "+E0x02000000" +global WS_EX_LAYERED := "+E0x00080000" -class CandidateBox extends Gui { - static min_width := 150 - static idx_col := 1 - static cand_col := 2 - static comment_col := 3 - static num_col := 3 +class CandidateBox { + pToken := 0 + gui := 0 + hDC := 0 + pBitmap := 0 + hBitmap := 0 + oBitmap := 0 + pGraphics := 0 + static isHidden := 1 __New() { - super.__New(, , this) - local back_color_val := UIStyle.back_color & 0xffffff - local text_color := Format("c{:x}", UIStyle.text_color & 0xffffff) - local font_point := Format("S{:d}", UIStyle.font_point) - local font_face := UIStyle.font_face - this.Opt("-DPIScale -Caption +Owner AlwaysOnTop " . WS_EX_NOACTIVATE) - this.MarginX := 3 - this.MarginY := 3 - this.BackColor := back_color_val - this.SetFont(Format("{} {}", font_point, text_color), font_face) - - this.pre := this.AddText(, "p") - this.pre.GetPos(, , , &h) - this.preedit_height := h - this.lv := this.AddListView("-Multi -Hdr -E0x200 LV0x10000 cWhite Background0x191919", ["i", "c", "m"]) - DllCall("uxtheme\SetWindowTheme", "ptr", this.lv.hwnd, "WStr", "DarkMode_Explorer", "Ptr", 0) - DllCall("uxtheme\SetWindowTheme", "ptr", this.lv.hwnd, "WStr", "DarkMode_ItemsView", "ptr", 0) - - this.dummy_lv1 := this.AddListView("-Multi -Hdr -E0x200 LV0x40 Hidden R1", ["p"]) - this.dummy_lv2 := this.AddListView("-Multi -Hdr -E0x200 LV0x40 Hidden R2", ["p"]) - this.dummy_lv1.GetPos(, , , &dh1) - this.dummy_lv2.GetPos(, , , &dh2) - this.row_height := dh2 - dh1 - this.row_padding := dh1 - this.row_height + if !this.pToken { + this.pToken := Gdip_Startup() + if !this.pToken { + MsgBox("GDI+ failed to start.") + ExitApp + } + } + ; +E0x8080088: WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST + this.gui := Gui("-Caption +E0x8080088 +LastFound -DPIScale +AlwaysOnTop", "CandidateBox") + this.dpiScale := GUIUtilities.GetMonitorDpiScale() + + this.UpdateUIStyle() + } + + __Delete() { + this.ReleaseAll() } UpdateUIStyle() { - local back_color_val := UIStyle.back_color & 0xffffff ; alpha not supported - local text_color := Format("c{:x}", UIStyle.text_color & 0xffffff) - local font_point := Format("S{:d}", UIStyle.font_point) - local font_face := UIStyle.font_face - this.BackColor := back_color_val - this.SetFont(Format("{} {}", font_point, text_color), font_face) - this.pre.SetFont(Format("{} {}", font_point, text_color), font_face) - this.lv.Opt(Format("{} Background0x{:x}", text_color, back_color_val)) - - if UIStyle.use_dark { - DllCall("uxtheme\SetWindowTheme", "ptr", this.lv.hwnd, "WStr", "DarkMode_Explorer", "ptr", 0) - DllCall("uxtheme\SetWindowTheme", "ptr", this.lv.hwnd, "WStr", "DarkMode_ItemsView", "ptr", 0) + this.borderWidth := UIStyle.border_width + this.borderColor := UIStyle.border_color + this.boxCornerR := UIStyle.corner_radius + this.hlCornerR := UIStyle.round_corner + this.lineSpacing := UIStyle.margin_y + this.padding := UIStyle.margin_x + + this.mainFontHs := this.CreateFontObj(UIStyle.font_face, UIStyle.font_point) + this.labFontHs := this.CreateFontObj(UIStyle.label_font_face, UIStyle.label_font_point) + this.commentFontHs := this.CreateFontObj(UIStyle.comment_font_face, UIStyle.comment_font_point) + ; fallback fonts + this.emojiFontHs := this.CreateFontObj("Segoe UI Emoji", UIStyle.font_point) + this.symbolFontHs := this.CreateFontObj("Segoe UI Symbol", UIStyle.font_point) + + ; preedite style + this.textColor := UIStyle.text_color + this.backgroundColor := UIStyle.back_color + this.hlTxtColor := UIStyle.hilited_text_color + this.hlBgColor := UIStyle.hilited_back_color + ; candidate style + this.hlCandTxtColor := UIStyle.hilited_candidate_text_color + this.hlCandBgColor := UIStyle.hilited_candidate_back_color + this.candTxtColor := UIStyle.candidate_text_color + this.candBgColor := UIStyle.candidate_back_color + + ; some color schemes have no these colors + this.labelColor := UIStyle.label_color + this.hlLabelColor := UIStyle.hilited_label_color + this.commentTxtColor := UIStyle.comment_text_color + this.hlCommentTxtColor := UIStyle.hilited_comment_text_color + } + + CreateFontObj(name, size) { + local em2pt := 96.0 / 72.0 + hFamily := Gdip_FontFamilyCreate(name) + hFont := Gdip_FontCreate(hFamily, size * em2pt * this.dpiScale, regular := 0) + hFormat := Gdip_StringFormatCreate(0x0001000 | 0x0004000) ; nowrap and noclip + Gdip_SetStringFormatAlign(hFormat, left := 0) ; left:0, center:1, right:2 + ; vertical align(top:0, center:1, bottom:2) + DllCall("gdiplus\GdipSetStringFormatLineAlign", "ptr", hFormat, "int", vCenter := 1) + return { hFamily: hFamily, hFont: hFont, hFormat: hFormat } + } + + Build(context, &winW, &winH) { ; build text layout + local menu := context.menu + local cands := menu.candidates + this.num_candidates := menu.num_candidates + this.hilited_index := menu.highlighted_candidate_index + 1 + + GetCompositionText(context.composition, &pre_selected, &selected, &post_selected) + this.prdSelTxt := pre_selected + this.prdHlSelTxt := selected + this.prdHlUnselTxt := post_selected + + local hDC := GetDC(this.gui.Hwnd) + local pGraphics := Gdip_GraphicsFromHDC(hDC) + + CreateRectF(&RC, 0, 0, 0, 0) + ; Build preedit layout + baseX := this.borderWidth + this.padding + baseY := this.borderWidth + this.lineSpacing + prdSelTxtSlicedInfo := this.MakeSlicedStrsInfo(pGraphics, baseX, baseY, this.prdSelTxt, this.mainFontHs, &prdSelTxtBoxSize, &RC) + prdHlSelTxtSize := this.MeasureString(pGraphics, this.prdHlSelTxt, this.mainFontHs, &RC) + prdHlSelTxtX := baseX + prdSelTxtBoxSize.w + this.padding + prdHlUnSelTxtSize := this.MeasureString(pGraphics, this.prdHlUnselTxt, this.mainFontHs, &RC) + prdHlUnSelTxtX := prdHlSelTxtX + prdHlSelTxtSize.w + this.preeditLayout := { + selBox: { size: prdSelTxtBoxSize, sliced: prdSelTxtSlicedInfo }, ; prdSelTxt may contains unicode symbols + hlSelBox: { size: prdHlSelTxtSize, rect: { x: prdHlSelTxtX, y: baseY, + w: prdHlSelTxtSize.w, h: prdHlSelTxtSize.h } }, + hlUnSelBox: { size: prdHlUnSelTxtSize, rect: { x: prdHlUnSelTxtX, y: baseY, w: prdHlUnSelTxtSize.w, h: prdHlUnSelTxtSize.h } }, + left: baseX, + top: baseY, + width: prdSelTxtBoxSize.w + this.padding + prdHlSelTxtSize.w + prdHlUnSelTxtSize.w, + height: Max(prdSelTxtBoxSize.h, prdHlSelTxtSize.h, prdHlUnSelTxtSize.h) + } + this.maxRowWidth := this.preeditLayout.width + + ; Build candidates layout + totalRowsHeight := this.preeditLayout.height + this.lineSpacing + baseY := baseY + totalRowsHeight + this.candidatesLayout := { labels: [], cands: [], comments: [], rows: [] } + + has_label := !!context.select_labels[0] + select_keys := menu.select_keys + num_select_keys := StrLen(select_keys) + + ; candidates may contain unicode symbols + Loop this.num_candidates { + labelText := String(A_Index) + if A_Index <= menu.page_size && has_label + labelText := context.select_labels[A_Index] || labelText + else if A_Index <= num_select_keys + labelText := SubStr(select_keys, A_Index, 1) + labelText := Format(UIStyle.label_format, labelText) + labelSlicedInfo := this.MakeSlicedStrsInfo(pGraphics, baseX, baseY, labelText, this.labFontHs, &labelBoxSize, &RC) + this.candidatesLayout.labels.Push({ size: labelBoxSize, sliced: labelSlicedInfo }) + + candText := cands[A_Index].text + candSlicedInfo := this.MakeSlicedStrsInfo(pGraphics, baseX + labelBoxSize.w, baseY, candText, this.mainFontHs, &candBoxSize, &RC) + this.candidatesLayout.cands.Push({ size: candBoxSize, sliced: candSlicedInfo }) + + commentText := cands[A_Index].comment + commentSlicedInfo := this.MakeSlicedStrsInfo(pGraphics, baseX + labelBoxSize.w + candBoxSize.w, baseY, commentText, this.commentFontHs, &commentBoxSize, &RC) + this.candidatesLayout.comments.Push({ size: commentBoxSize, sliced: commentSlicedInfo }) + + rowRect := { + x: baseX, y: baseY, + w: labelBoxSize.w + candBoxSize.w + (commentText ? this.padding * 2 + commentBoxSize.w : 0), + h: Max(labelBoxSize.h, candBoxSize.h, commentBoxSize.h) + } + this.candidatesLayout.rows.Push(rowRect) + if (rowRect.w > this.maxRowWidth) { + this.maxRowWidth := rowRect.w + } + increment := rowRect.h + this.lineSpacing + baseY += increment, totalRowsHeight += increment + } + totalRowsHeight -= this.lineSpacing ; remove extra line spacing + + Gdip_DeleteGraphics(pGraphics) + ReleaseDC(hDC, this.gui.Hwnd) + + this.commentOffset := 0 + this.boxWidth := Ceil(this.maxRowWidth) + (this.borderWidth + this.padding) * 2 + if this.boxWidth < UIStyle.min_width { + this.commentOffset := UIStyle.min_width - this.boxWidth + this.boxWidth := UIStyle.min_width + } + this.boxHeight := Ceil(totalRowsHeight) + (this.borderWidth + this.padding) * 2 + winW := this.boxWidth + winH := this.boxHeight + + ; get better spacing to align comments + loop this.num_candidates { + labelW := this.candidatesLayout.labels[A_Index].size.w + candW := this.candidatesLayout.cands[A_Index].size.w + commentW := this.candidatesLayout.comments[A_Index].size.w + if commentW > 0 { + alignCommentGap := this.maxRowWidth - labelW - candW - commentW + for _, info in this.candidatesLayout.comments[A_Index].sliced + info.x := info.x + alignCommentGap + this.commentOffset + } + } + } + + Show(x, y) { + if (this.gui && CandidateBox.isHidden) { + this.gui.Show("NA") + CandidateBox.isHidden := 0 + } + + this.hDC := CreateCompatibleDC() + this.hBitmap := CreateDIBSection(this.boxWidth, this.boxHeight) + this.oBitmap := SelectObject(this.hDC, this.hBitmap) + this.pGraphics := Gdip_GraphicsFromHDC(this.hDC) + Gdip_SetTextRenderingHint(this.pGraphics, AntiAliasGridFit := 3) + Gdip_SetSmoothingMode(this.pGraphics, AntiAlias := 4) + + ; Draw border + if (this.borderWidth > 0) { + pBrushBorder := Gdip_BrushCreateSolid(this.borderColor) + this.FillRoundedRect(this.pGraphics, pBrushBorder, 0, 0, this.boxWidth, this.boxHeight, this.boxCornerR) + Gdip_DeleteBrush(pBrushBorder) + } + + ; Draw background + pBrushBg := Gdip_BrushCreateSolid(this.backgroundColor) + bgX := this.borderWidth, bgY := this.borderWidth + bgW := this.boxWidth - this.borderWidth * 2, bgH := this.boxHeight - this.borderWidth * 2 + bgCornerRadius := this.boxCornerR > this.borderWidth ? this.boxCornerR - this.borderWidth : 0 + this.FillRoundedRect(this.pGraphics, pBrushBg, bgX, bgY, bgW, bgH, bgCornerRadius) + Gdip_DeleteBrush(pBrushBg) + + ; Draw preedit + rectShrink := 2 + this.DrawSlicedTexts(this.pGraphics, this.mainFontHs, this.preeditLayout.height, this.preeditLayout.selBox.sliced, this.textColor) + prdHlSelTxtRc := this.preeditLayout.hlSelBox.rect + prdHlUnselTxtRc := this.preeditLayout.hlUnSelBox.rect + if this.prdHlSelTxt { + pBrsh_hlSelBg := Gdip_BrushCreateSolid(this.hlBgColor) + Gdip_FillRoundedRectangle(this.pGraphics, pBrsh_hlSelBg, prdHlSelTxtRc.x - rectShrink, prdHlSelTxtRc.y, prdHlSelTxtRc.w, prdHlSelTxtRc.h - rectShrink, this.hlCornerR) + Gdip_DeleteBrush(pBrsh_hlSelBg) + } + this.DrawText(this.pGraphics, this.mainFontHs, this.prdHlSelTxt, prdHlSelTxtRc, this.hlTxtColor) + this.DrawText(this.pGraphics, this.mainFontHs, this.prdHlUnselTxt, prdHlUnselTxtRc, this.textColor) + + hiliteW := this.boxWidth - this.borderWidth * 2 - this.padding * 2 + ; Draw candidates + Loop this.num_candidates { + rowRect := this.candidatesLayout.rows[A_Index] + labelFg := this.labelColor + candFg := this.candTxtColor + commentFg := this.commentTxtColor + if (A_Index == this.hilited_index) { ; Draw highlight if selected + labelFg := this.hlLabelColor + candFg := this.hlCandTxtColor + commentFg := this.hlCommentTxtColor + pBrsh_hlCandBg := Gdip_BrushCreateSolid(this.hlCandBgColor) + Gdip_FillRoundedRectangle(this.pGraphics, pBrsh_hlCandBg, rowRect.x, rowRect.y, hiliteW, rowRect.h, this.hlCornerR) + Gdip_DeleteBrush(pBrsh_hlCandBg) + } + + this.DrawSlicedTexts(this.pGraphics, this.labFontHs, rowRect.h, this.candidatesLayout.labels[A_Index].sliced, labelFg) + this.DrawSlicedTexts(this.pGraphics, this.mainFontHs, rowRect.h, this.candidatesLayout.cands[A_Index].sliced, candFg) + + commentLayout := this.candidatesLayout.comments[A_Index] + if commentLayout.size.w > 0 { + this.DrawSlicedTexts(this.pGraphics, this.commentFontHs, rowRect.h, commentLayout.sliced, commentFg) + } + } + + UpdateLayeredWindow(this.gui.Hwnd, this.hDC, x, y, this.boxWidth, this.boxHeight) + this.ReleaseDrawingSurface() + } + + Hide() { + if (this.gui && !CandidateBox.isHidden) { + this.gui.Show("Hide") + CandidateBox.isHidden := 1 + } + } + + ReleaseFonts() { + DeleteFont(this.mainFontHs) + DeleteFont(this.labFontHs) + DeleteFont(this.commentFontHs) + DeleteFont(this.emojiFontHs) + DeleteFont(this.symbolFontHs) + + DeleteFont(fntHs) { + if (fntHs.hFont) + Gdip_DeleteFont(fntHs.hFont), fntHs.hFont := 0 + if (fntHs.hFamily) + Gdip_DeleteFontFamily(fntHs.hFamily), fntHs.hFamily := 0 + if (fntHs.hFormat) + Gdip_DeleteStringFormat(fntHs.hFormat), fntHs.hFormat := 0 + } + } + + ReleaseDrawingSurface() { + if (this.pGraphics) { + Gdip_DeleteGraphics(this.pGraphics) + this.pGraphics := 0 + } + if (this.hDC && this.hBitmap) { + SelectObject(this.hDC, this.oBitmap), DeleteObject(this.hBitmap) + DeleteDC(this.hDC) + this.oBitmap := 0, this.hBitmap := 0 + this.hDC := 0 + } + } + + ReleaseAll() { + this.ReleaseFonts() + this.ReleaseDrawingSurface() + + if (this.pToken) { + Gdip_Shutdown(this.pToken) + this.pToken := 0 + } + if (this.gui) { + this.gui.Destroy() + } + } + + MeasureString(pGraphics, text, fontHs, &RectF) { + if !text + return { w: 0, h: 0 } + + rc := Buffer(16) + DllCall("gdiplus\GdipMeasureString", + "Ptr", pGraphics, + "WStr", text, + "Int", -1, + "Ptr", fontHs.hFont, + "Ptr", RectF.Ptr, + "Ptr", fontHs.hFormat, + "Ptr", rc.Ptr, + "UInt*", 0, + "UInt*", 0, + "Int") + + return { w: NumGet(rc.Ptr, 8, "Float"), h: NumGet(rc.Ptr, 12, "Float") } + } + + MakeSymbolsInfo(pGraphics, xOffset, yOffset, sType, cpArr, &symsBoxSize, &RectF) { + symsInfoArr := [] + totalSymRowW := 0, maxSymRowH := 0 + curX := xOffset, rowY := yOffset + isEmoji := sType == 2 + loop cpArr.Length { + curCP := cpArr[A_Index] + symSize := this.MeasureString(pGraphics, curCP, isEmoji ? this.emojiFontHs : this.symbolFontHs, &RectF) + + totalSymRowW += symSize.w + if (symSize.h > maxSymRowH) + maxSymRowH := symSize.h + + symsInfoArr.Push({ + x: curX, + y: rowY, + w: symSize.w, + h: symSize.h, + isEmoji: isEmoji + }) + + curX += symSize.w + } + + symsBoxSize := { w: totalSymRowW, h: maxSymRowH } + return symsInfoArr + } + + MakeSlicedStrsInfo(pGraphics, xOffset, yOffset, text, fontHs, &textBoxSize, &RC) { + if !text { + textBoxSize := { w: 0, h: 0 } + return [] + } + + ; slice text to pieces by emojis and special Symbols + pattern := "([\x{1F000}-\x{1FAFF}\x{1F900}-\x{1F9FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{1D400}-\x{1D7FF}\x{200D}\x{FE0F}\x{FE0E}]+)" + + pieces := [] + pos := 1 + while (RegExMatch(text, pattern, &m, pos)) { + start := m.Pos(1), length := m.Len(1) + ; group normal text + if (start > pos) { + normal := SubStr(text, pos, start - pos) + if (normal != "") + pieces.Push({ sType: 0, value: normal }) + } + ; group emoji/unicode symbols with codePoints + cpArr := getCodePoints(m[1]) + if RegExMatch(m[1], "^[\x{1F300}-\x{1FAFF}\x{1F900}-\x{1F9FF}\x{200D}\x{FE0F}\x{FE0E}]$") { + pieces.Push({ sType: 2, value: cpArr }) + } else { + pieces.Push({ sType: 1, value: cpArr }) + } + + pos := start + length + } + ; group remaining normal text + if (pos <= StrLen(text)) { + rest := SubStr(text, pos) + if (rest != "") + pieces.Push({ sType: 0, value: rest }) + } + + ; build sliced strings info + slicedInfo := [] + totalRowW := 0, maxRowH := 0 + curX := xOffset, curY := yOffset + for i, p in pieces { + sType := p.sType + content := p.value + slicedSize := { w: 0, h: 0 } + slicedSymsInfo := [] + if sType { + slicedSymsInfo := this.MakeSymbolsInfo(pGraphics, curX, curY, sType, content, &symsBoxSize, &RC) + slicedSize := symsBoxSize + } else { + slicedSize := this.MeasureString(pGraphics, content, fontHs, &RC) + } + totalRowW += slicedSize.w + if (slicedSize.h > maxRowH) + maxRowH := slicedSize.h + + slicedInfo.Push({ + x: curX, + y: curY, + w: slicedSize.w, + h: slicedSize.h, + sType: sType, + slicedSymsInfo: slicedSymsInfo, + value: content + }) + + curX += slicedSize.w + } + + textBoxSize := { w: totalRowW, h: maxRowH } + return slicedInfo + + getCodePoints(str) { + arr := [] + i := 1, len := StrLen(str) + + ; skip ZWJ, VS15/VS16 as GDI does not support color fonts + while (i <= len) { + cp := Ord(SubStr(str, i, 1)) + if (cp = 0x200D or cp = 0xFE0F or cp = 0xFE0E) { + i++ + } else if cp >= 0xD800 && cp <= 0xDBFF && i < len { + low := Ord(SubStr(str, i + 1, 1)) + if (low >= 0xDC00 && low <= 0xDFFF) { + arr.Push(Chr(0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00))) + i += 2 + } else { + arr.Push(Chr(cp)) + i++ + } + } else { + arr.Push(Chr(cp)) + i++ + } + } + + return arr + } + } + + DrawText(pGraphics, fontHs, text, textRect, color) { + this.pBrush := Gdip_BrushCreateSolid(color) + CreateRectF(&RC, textRect.x, textRect.y, textRect.w, textRect.h) + Gdip_DrawString(pGraphics, text, fontHs.hFont, fontHs.hFormat, this.pBrush, &RC) + Gdip_DeleteBrush(this.pBrush) + } + + DrawSlicedTexts(pGraphics, fontHs, rowMaxH, slicedStrsInfo, color) { + this.pBrush := Gdip_BrushCreateSolid(color) + loop slicedStrsInfo.Length { + info := slicedStrsInfo[A_Index] + info.h := rowMaxH + if info.sType + drawEmojiAndSymbols(this.pGraphics, this.pBrush, info) + else + drawNormalText(this.pGraphics, this.pBrush, fontHs, info) + } + Gdip_DeleteBrush(this.pBrush) + + drawNormalText(pGraphics, pBrush, fontHs, textInfo) { + CreateRectF(&RC, textInfo.x, textInfo.y, textInfo.w, textInfo.h) + Gdip_DrawString(pGraphics, textInfo.value, fontHs.hFont, fontHs.hFormat, pBrush, &RC) + } + + drawEmojiAndSymbols(pGraphics, pBrush, textInfo) { + cpArr := textInfo.value + uSymsMaxH := textInfo.h + uSymsInfo := textInfo.slicedSymsInfo + loop cpArr.Length { + CreateRectF(&RC, uSymsInfo[A_Index].x, uSymsInfo[A_Index].y, uSymsInfo[A_Index].w, uSymsMaxH) + if uSymsInfo[A_Index].isEmoji + Gdip_DrawString(pGraphics, cpArr[A_Index], this.emojiFontHs.hFont, this.emojiFontHs.hFormat, pBrush, &RC) + else { ; is unicode Symbols + Gdip_DrawString(pGraphics, cpArr[A_Index], this.symbolFontHs.hFont, this.symbolFontHs.hFormat, pBrush, &RC) + } + } + } + } + + FillRoundedRect(pGraphics, pBrush, x, y, w, h, r) { + if (r <= 0) { + Gdip_FillRectangle(pGraphics, pBrush, x, y, w, h) } else { - DllCall("uxtheme\SetWindowTheme", "ptr", this.lv.hwnd, "WStr", "Explorer", "Ptr", 0) - DllCall("uxtheme\SetWindowTheme", "ptr", this.lv.hwnd, "WStr", "ItemsView", "Ptr", 0) + Gdip_FillRoundedRectangle(pGraphics, pBrush, x, y, w, h, r) + } + } +} + +class LegacyCandidateBox { + static dbg := false + static gui := 0 + static border := LegacyCandidateBox.dbg ? "+border" : 0 + + __New() { + this.UpdateUIStyle() + } + + UpdateUIStyle() { + ; alpha not supported + del_opaque(color) { + return color & 0xffffff + } + LegacyCandidateBox.text_color := del_opaque(UIStyle.text_color) + LegacyCandidateBox.back_color := del_opaque(UIStyle.back_color) + LegacyCandidateBox.candidate_text_color := del_opaque(UIStyle.candidate_text_color) + LegacyCandidateBox.candidate_back_color := del_opaque(UIStyle.candidate_back_color) + LegacyCandidateBox.label_color := del_opaque(UIStyle.label_color) + LegacyCandidateBox.comment_text_color := del_opaque(UIStyle.comment_text_color) + LegacyCandidateBox.hilited_text_color := del_opaque(UIStyle.hilited_text_color) + LegacyCandidateBox.hilited_back_color := del_opaque(UIStyle.hilited_back_color) + LegacyCandidateBox.hilited_candidate_text_color := del_opaque(UIStyle.hilited_candidate_text_color) + LegacyCandidateBox.hilited_candidate_back_color := del_opaque(UIStyle.hilited_candidate_back_color) + LegacyCandidateBox.hilited_label_color := del_opaque(UIStyle.hilited_label_color) + LegacyCandidateBox.hilited_comment_text_color := del_opaque(UIStyle.hilited_comment_text_color) + + LegacyCandidateBox.base_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.text_color, LegacyCandidateBox.back_color, LegacyCandidateBox.border) + LegacyCandidateBox.candidate_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.candidate_text_color, LegacyCandidateBox.candidate_back_color, LegacyCandidateBox.border) + LegacyCandidateBox.label_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.label_color, LegacyCandidateBox.candidate_back_color, LegacyCandidateBox.border) + LegacyCandidateBox.comment_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.comment_text_color, LegacyCandidateBox.candidate_back_color, LegacyCandidateBox.border) + LegacyCandidateBox.hilited_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.hilited_text_color, LegacyCandidateBox.hilited_back_color, LegacyCandidateBox.border) + LegacyCandidateBox.hilited_candidate_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.hilited_candidate_text_color, LegacyCandidateBox.hilited_candidate_back_color, LegacyCandidateBox.border) + LegacyCandidateBox.hilited_label_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.hilited_label_color, LegacyCandidateBox.hilited_candidate_back_color, LegacyCandidateBox.border) + LegacyCandidateBox.hilited_comment_opt := Format("c{:x} Background{:x} {}", LegacyCandidateBox.hilited_comment_text_color, LegacyCandidateBox.hilited_candidate_back_color, LegacyCandidateBox.border) + + LegacyCandidateBox.base_font_opt := Format("s{} q5", UIStyle.font_point) + LegacyCandidateBox.label_font_opt := Format("s{} q5", UIStyle.label_font_point) + LegacyCandidateBox.comment_font_opt := Format("s{} q5", UIStyle.comment_font_point) + + if LegacyCandidateBox.gui { + LegacyCandidateBox.gui.BackColor := LegacyCandidateBox.back_color + LegacyCandidateBox.gui.MarginX := UIStyle.margin_x + LegacyCandidateBox.gui.MarginY := UIStyle.margin_y + + if HasProp(LegacyCandidateBox.gui, "pre") && LegacyCandidateBox.gui.pre + LegacyCandidateBox.gui.pre.Opt(LegacyCandidateBox.base_opt) + if HasProp(LegacyCandidateBox.gui, "sel") && LegacyCandidateBox.gui.sel + LegacyCandidateBox.gui.sel.Opt(LegacyCandidateBox.hilited_opt) + if HasProp(LegacyCandidateBox.gui, "post") && LegacyCandidateBox.gui.post + LegacyCandidateBox.gui.post.Opt(LegacyCandidateBox.base_opt) } } Build(context, &width, &height) { - local has_selected := GetCompositionText(context.composition, &pre_selected, &selected, &post_selected) - local cands := context.menu.candidates - local lv_height := this.row_height * context.menu.num_candidates + this.row_padding - - preedit_text := pre_selected - if has_selected - preedit_text := preedit_text . "[" . selected "]" . post_selected - - this.pre.Value := preedit_text - this.dummy_lv1.Delete() - this.dummy_lv1.Add(, preedit_text) - this.dummy_lv1.ModifyCol() - preedit_width := SendMessage(LVM_GETCOLUMNWIDTH, 0, 0, this.dummy_lv1) - - this.lv.Delete() - has_comment := false - Loop context.menu.num_candidates { - opt := (A_Index == context.menu.highlighted_candidate_index + 1) ? "Select" : "" - if comment := cands[A_Index].comment - has_comment := true - this.lv.Add(opt, A_Index . ". ", cands[A_Index].text, comment) - } - - total_width := 0 - this.lv.ModifyCol() - if not has_comment - this.lv.ModifyCol(CandidateBox.comment_col, 0) - this.lv.GetPos(, , , &cands_height) - Loop CandidateBox.num_col { - width := SendMessage(LVM_GETCOLUMNWIDTH, A_Index - 1, 0, this.lv) - total_width += width - if A_Index == CandidateBox.cand_col - cand_width := width - } - - if not cand_width - cand_width := SendMessage(LVM_GETCOLUMNWIDTH, CandidateBox.cand_col - 1, 0, this.lv) - - max_width := Max(preedit_width, total_width, CandidateBox.min_width) - if total_width < max_width - this.lv.ModifyCol(CandidateBox.cand_col, cand_width + max_width - total_width) - - this.lv.Move(, , max_width, lv_height) - this.pre.Move(, , max_width) - this.lv.Redraw() - - width := max_width + 6 - height := this.preedit_height + lv_height + this.MarginY + if !LegacyCandidateBox.gui || !LegacyCandidateBox.gui.built + LegacyCandidateBox.gui := LegacyCandidateBox.BoxGui(context) + else + LegacyCandidateBox.gui.Update(context) + width := LegacyCandidateBox.gui.max_width + height := LegacyCandidateBox.gui.max_height + } + + Show(x, y) { + LegacyCandidateBox.gui.Show(Format("AutoSize NA x{} y{}", x, y)) + } + + Hide() { + if LegacyCandidateBox.gui && HasMethod(LegacyCandidateBox.gui, "Show") + LegacyCandidateBox.gui.Show("Hide") + } + + class BoxGui extends Gui { + built := false + __New(context, &pre?, &sel?, &post?, &menu?) { + super.__New(, , this) + + menu := context.menu + local cands := menu.candidates + local num_candidates := menu.num_candidates + local hilited_index := menu.highlighted_candidate_index + 1 + local composition := context.composition + GetCompositionText(composition, &pre, &sel, &post) + + this.Opt(Format("-DPIScale -Caption +Owner +AlwaysOnTop {} {} {}", WS_EX_NOACTIVATE, WS_EX_COMPOSITED, WS_EX_LAYERED)) + this.BackColor := LegacyCandidateBox.back_color + this.SetFont(LegacyCandidateBox.base_font_opt, UIStyle.font_face) + this.MarginX := UIStyle.margin_x + this.MarginY := UIStyle.margin_y + this.num_candidates := num_candidates + this.has_comment := false + + ; build preedit + this.max_width := 0 + this.preedit_height := 0 + local head_position := Format("x{} y{} section {}", this.MarginX, this.MarginY, LegacyCandidateBox.border) + local position := head_position + if pre { + this.pre := this.AddText(position, pre) + this.pre.Opt(LegacyCandidateBox.base_opt) + position := Format("x+{} ys {}", this.MarginX, LegacyCandidateBox.border) + this.pre.GetPos(, , &w, &h) + this.preedit_height := max(this.preedit_height, h) + this.pre_width := w + this.max_width += (w + this.MarginX) + } + if sel { + this.sel := this.AddText(position, sel) + this.sel.Opt(LegacyCandidateBox.hilited_opt) + position := Format("x+{} ys {}", this.MarginX, LegacyCandidateBox.border) + this.sel.GetPos(, , &w, &h) + this.preedit_height := max(this.preedit_height, h) + this.sel_width := w + this.max_width += (w + this.MarginX) + } + if post { + this.post := this.AddText(position, post) + this.post.Opt(LegacyCandidateBox.base_opt) + this.post.GetPos(, , &w, &h) + this.preedit_height := max(this.preedit_height, h) + this.post_width := w + this.max_width += w + } + + ; build candidates + this.max_label_width := 0 + this.max_candidate_width := 0 + this.max_comment_width := 0 + this.candidate_height := 0 + local has_label := !!context.select_labels[0] + local select_keys := menu.select_keys + local num_select_keys := StrLen(select_keys) + loop num_candidates { + position := Format("xs y+{} section {}", this.MarginY, LegacyCandidateBox.border) + local label_text := String(A_Index) + if A_Index <= menu.page_size && has_label + label_text := context.select_labels[A_Index] + else if A_Index <= num_select_keys + label_text := SubStr(select_keys, A_Index, 1) + label_text := Format(UIStyle.label_format, label_text) + this.SetFont(LegacyCandidateBox.label_font_opt, UIStyle.label_font_face) + local label := this.AddText(Format("Right {} vL{}", position, A_Index), label_text) + label.GetPos(, , &w, &h1) + this.max_label_width := max(this.max_label_width, w + this.MarginX) + + position := Format("x+{} ys {}", this.MarginX, LegacyCandidateBox.border) + this.SetFont(LegacyCandidateBox.base_font_opt, UIStyle.font_face) + local candidate := this.AddText(Format("{} vC{}", position, A_Index), cands[A_Index].text) + candidate.GetPos(, , &w, &h2) + this.max_candidate_width := max(this.max_candidate_width, w + this.MarginX) + + if comment_text := cands[A_Index].comment + this.has_comment := true + this.SetFont(LegacyCandidateBox.comment_font_opt, UIStyle.comment_font_face) + local comment := this.AddText(Format("{} vM{}", position, A_Index), comment_text) + comment.GetPos(, , &w, &h3) + comment.Opt(Format("c{:x}", LegacyCandidateBox.comment_text_color)) + comment.Visible := this.has_comment + this.max_comment_width := max(this.max_comment_width, w) + this.candidate_height := max(this.candidate_height, h1, h2, h3) + + if A_Index == hilited_index { + label.Opt(LegacyCandidateBox.hilited_label_opt) + candidate.Opt(LegacyCandidateBox.hilited_candidate_opt) + comment.Opt(LegacyCandidateBox.hilited_comment_opt) + } else { + label.Opt(LegacyCandidateBox.label_opt) + candidate.Opt(LegacyCandidateBox.candidate_opt) + comment.Opt(LegacyCandidateBox.comment_opt) + } + } + + ; adjust width height + local list_width := this.max_label_width + this.max_candidate_width + this.has_comment * this.max_comment_width + local box_width := max(UIStyle.min_width, list_width) + if box_width > this.max_width && HasProp(this, "post") && this.post + this.post.Move(, , this.post_width + box_width - this.max_width) + this.max_width := max(box_width, this.max_width) + if this.max_width > list_width { + this.max_candidate_width += this.max_width - list_width + loop num_candidates + this["C" . A_Index].Move(, , this.max_candidate_width) + } + local y := 2 * this.MarginY + this.preedit_height + loop num_candidates { + local x := this.MarginX + this["L" . A_Index].Move(x, y, this.max_label_width) + this["L" . A_Index].GetPos(, , , &h) + local max_h := h + x += this.max_label_width + this["C" . A_Index].Move(x, y, this.max_candidate_width) + this["C" . A_Index].GetPos(, , , &h) + max_h := max(max_h, h) + x += this.max_candidate_width + this["M" . A_Index].Move(x, y, this.max_comment_width) + this["M" . A_Index].GetPos(, , , &h) + max_h := max(max_h, h) + y += (max_h + this.MarginY) + } + this.max_height := y + this.max_width += (2 * this.MarginX) + + this.built := true + } + + Update(context) { + local fake_gui := LegacyCandidateBox.BoxGui(context, &pre, &sel, &post, &menu) + local num_candidates := menu.num_candidates + local hilited_index := menu.highlighted_candidate_index + 1 + this.SetFont(LegacyCandidateBox.base_font_opt, UIStyle.font_face) + this.num_candidates := max(this.num_candidates, num_candidates) + this.max_width := fake_gui.max_width + this.max_height := fake_gui.max_height + + ; reset preedit + if pre { + if !HasProp(this, "pre") || !this.pre + this.pre := this.AddText(, pre) + this.pre.Value := fake_gui.pre.Value + fake_gui.pre.GetPos(&x, &y, &w, &h) + this.pre.Move(x, y, w, h) + } + if HasProp(this, "pre") && this.pre + this.pre.Visible := !!pre + if sel { + if !HasProp(this, "sel") || !this.sel + this.sel := this.AddText(, sel) + this.sel.Value := fake_gui.sel.Value + fake_gui.sel.GetPos(&x, &y, &w, &h) + this.sel.Move(x, y, w, h) + } + if HasProp(this, "sel") && this.sel + this.sel.Visible := !!sel + if post { + if !HasProp(this, "post") || !this.post + this.post := this.AddText(, post) + this.post.Value := fake_gui.post.Value + fake_gui.post.GetPos(&x, &y, &w, &h) + this.post.Move(x, y, w, h) + } + if HasProp(this, "post") && this.post + this.post.Visible := !!post + + ; reset candidates + loop this.num_candidates { + if A_Index > num_candidates { + this["L" . A_Index].Visible := false + this["C" . A_Index].Visible := false + this["M" . A_Index].Visible := false + continue + } + local fake_label := fake_gui["L" . A_Index] + local fake_candidate := fake_gui["C" . A_Index] + local fake_comment := fake_gui["M" . A_Index] + this.SetFont(LegacyCandidateBox.label_font_opt, UIStyle.label_font_face) + try + local label := this["L" . A_Index] + catch + local label := this.AddText(Format("vL{}", A_Index), fake_label.Value) + this.SetFont(LegacyCandidateBox.base_font_opt, UIStyle.font_face) + try + local candidate := this["C" . A_Index] + catch + local candidate := this.AddText(Format("vC{}", A_Index), fake_candidate.Value) + this.SetFont(LegacyCandidateBox.comment_font_opt, UIStyle.comment_font_face) + try + local comment := this["M" . A_Index] + catch + local comment := this.AddText(Format("vM{}", A_Index), fake_comment.Value) + label.Value := fake_label.Value + fake_label.GetPos(&x, &y, &w, &h) + label.Move(x, y, w, h) + candidate.Value := fake_candidate.Value + fake_candidate.GetPos(&x, &y, &w, &h) + candidate.Move(x, y, w, h) + comment.Value := fake_comment.Value + fake_comment.GetPos(&x, &y, &w, &h) + comment.Move(x, y, w, h) + + if A_Index == hilited_index { + label.Opt(LegacyCandidateBox.hilited_label_opt) + candidate.Opt(LegacyCandidateBox.hilited_candidate_opt) + comment.Opt(LegacyCandidateBox.hilited_comment_opt) + } else { + label.Opt(LegacyCandidateBox.label_opt) + candidate.Opt(LegacyCandidateBox.candidate_opt) + comment.Opt(LegacyCandidateBox.comment_opt) + } + local visible := (A_Index <= num_candidates) + label.Visible := visible + candidate.Visible := visible + comment.Visible := (fake_gui.has_comment && visible) + } + + fake_gui.GetPos(, , &width, &height) + this.Move(, , width, height) + } } } @@ -178,23 +870,3 @@ GetCompositionText(composition, &pre_selected, &selected, &post_selected) { return false } } - -GetMenuText(menu) { - local text := "" - if menu.num_candidates == 0 - return text - local cands := menu.candidates - Loop menu.num_candidates { - local is_highlighted := (A_Index == menu.highlighted_candidate_index + 1) - if A_Index > 1 - text := text . "`r`n" - text := text . Format("{}. {}{}{}{}", - A_Index, - (is_highlighted ? "[" : " "), - cands[A_Index].text, - (is_highlighted ? "]" : " "), - cands[A_Index].comment - ) - } - return text -} diff --git a/Lib/RabbitCommon.ahk b/Lib/RabbitCommon.ahk index e8ebf44..840a287 100644 --- a/Lib/RabbitCommon.ahk +++ b/Lib/RabbitCommon.ahk @@ -46,6 +46,7 @@ global RABBIT_FULL_MAINTENANCE := "2" global IN_MAINTENANCE := false global STATUS_TOOLTIP := 2 global box := 0 +global rabbit_traits global IS_DARK_MODE := false global ASCII_MODE_FALSE_LABEL := "中文" global ASCII_MODE_TRUE_LABEL := "西文" @@ -104,6 +105,10 @@ CreateTraits() { } RabbitUserDataPath() { + if FileExist(A_ScriptDir . "\.portable") { + RabbitDebug("run in portable mode.", Format("RabbitCommon.ahk:{}", A_LineNumber), 1) + return A_ScriptDir . "\Rime" + } try { local dir := RegRead("HKEY_CURRENT_USER\Software\Rime\Rabbit", "RimeUserDir") } @@ -170,3 +175,33 @@ CleanOldLogs() { } } } + +RabbitLog(text) { + try { + FileAppend(text, "*", "UTF-8") + } +} +RabbitLogLimit(text, label, limit := 1) { + static labels := Map() + if !labels.Has(label) + labels[label] := 0 + if limit < 0 || labels[label] < limit { + RabbitLog(text) + labels[label] := labels[label] + 1 + } +} +RabbitError(text, location, limit := -1) { + msg := Format("E{} {:5} {}] {}`r`n", FormatTime(, "yyyyMMdd HH:mm:ss "), ProcessExist(), location, text) + RabbitLogLimit(msg, location, limit) +} +RabbitInfo(text, location, limit := -1) { + msg := Format("I{} {:5} {}] {}`r`n", FormatTime(, "yyyyMMdd HH:mm:ss "), ProcessExist(), location, text) + RabbitLogLimit(msg, location, limit) +} +RabbitDebug(text, location, limit := -1) { + global RABBIT_VERSION + if !SubStr(RABBIT_VERSION, 1, 3) = "dev" + return + msg := Format("D{} {:5} {}] {}`r`n", FormatTime(, "yyyyMMdd HH:mm:ss "), ProcessExist(), location, text) + RabbitLogLimit(msg, location, limit) +} diff --git a/Lib/RabbitConfig.ahk b/Lib/RabbitConfig.ahk index b50c9ca..f5beaca 100644 --- a/Lib/RabbitConfig.ahk +++ b/Lib/RabbitConfig.ahk @@ -27,6 +27,7 @@ class RabbitConfig { static preset_process_ascii := Map() static schema_icon := Map() static fix_candidate_box := false + static use_legacy_candidate_box := false static load() { global rime, IS_DARK_MODE @@ -58,11 +59,14 @@ class RabbitConfig { if rime.config_test_get_bool(config, "fix_candidate_box", &result) RabbitConfig.fix_candidate_box := !!result + if rime.config_test_get_bool(config, "use_legacy_candidate_box", &result) + RabbitConfig.use_legacy_candidate_box := !!result UIStyle.Update(config, true) if IS_DARK_MODE := RabbitIsUserDarkMode() { if color_name := rime.config_get_string(config, "style/color_scheme_dark") UIStyle.use_dark := UIStyle.UpdateColor(config, color_name) + DarkMode.set(IS_DARK_MODE) } rime.config_close(config) diff --git a/Lib/RabbitThemesUI.ahk b/Lib/RabbitThemesUI.ahk new file mode 100644 index 0000000..13a0b9b --- /dev/null +++ b/Lib/RabbitThemesUI.ahk @@ -0,0 +1,428 @@ +/* + * Copyright (c) 2005 Tim + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#Include +#Include + +class CandidatePreview { + pToken := 0 + pBitmap := 0 + hBitmap := 0 + pGraphics := 0 + hFont := 0 + hFamily := 0 + hFormat := 0 + + __New(ctrl, theme, &calcW, &calcH) { + if !this.pToken { + this.pToken := Gdip_Startup() + if !this.pToken { + MsgBox("GDI+ failed to start.") + ExitApp + } + } + this.imgCtrl := ctrl + this.dpiSacle := GUIUtilities.GetMonitorDpiScale() + + this.borderWidth := UIStyle.border_width + this.borderColor := UIStyle.border_color + this.cornerRadius := UIStyle.corner_radius + this.lineSpacing := UIStyle.margin_y + this.padding := UIStyle.margin_x + + ; only use one font to preview + this.fontName := theme.HasOwnProp("font_face") ? theme.font_face : UIStyle.font_face + this.fontSize := theme.HasOwnProp("font_point") ? theme.font_point : UIStyle.font_point + ; preedite style + this.borderColor := theme.border_color + this.textColor := theme.text_color + this.backgroundColor := theme.back_color + this.hlTxtColor := theme.hilited_text_color + this.hlBgColor := theme.hilited_back_color + ; candidate style + this.hlCandTxtColor := theme.hilited_candidate_text_color + this.hlCandBgColor := theme.hilited_candidate_back_color + this.candTxtColor := theme.candidate_text_color + this.candBgColor := theme.candidate_back_color + this.CalcSize(&calcW, &calcH) + } + + __Delete() { + this.ReleaseAll() + } + + CalcSize(&calcW, &calcH) { + this.hFamily := Gdip_FontFamilyCreate(this.fontName) + this.hFont := Gdip_FontCreate(this.hFamily, this.fontSize * this.dpiSacle, regular := 0) + this.hFormat := Gdip_StringFormatCreate(0x0001000 | 0x0004000) + Gdip_SetStringFormatAlign(this.hFormat, left := 0) ; left:0, center:1, right:2 + + hDC := GetDC(this.imgCtrl.Hwnd) + pGraphics := Gdip_GraphicsFromHDC(hDC) + + CreateRectF(&RC, 0, 0, 0, 0) + this.prdSelSize := this.MeasureString(pGraphics, "RIME", this.hFont, this.hFormat, &RC) + this.prdHlSize := this.MeasureString(pGraphics, "shu ru fa", this.hFont, this.hFormat, &RC) + this.candSize := this.MeasureString(pGraphics, "1. 输入法", this.hFont, this.hFormat, &RC) + + this.maxRowWidth := this.prdSelSize.w + this.padding + this.prdHlSize.w + totalHeight := (this.prdSelSize.h + this.lineSpacing) * 6 + + Gdip_DeleteGraphics(pGraphics) + ReleaseDC(hDC, this.imgCtrl.Hwnd) + + this.previewWidth := Ceil(this.maxRowWidth) + this.padding * 2 + this.borderWidth * 2 + this.previewHeight := Ceil(totalHeight) + this.padding * 2 + this.borderWidth * 2 - this.lineSpacing ; Remove last line spacing + calcW := this.previewWidth + calcH := this.previewHeight + } + + Render(candsArray, selIndex) { + ; Create a bitmap in memory that matches the size of preview + this.pBitmap := Gdip_CreateBitmap(this.previewWidth, this.previewHeight) + this.pGraphics := Gdip_GraphicsFromImage(this.pBitmap) + Gdip_SetSmoothingMode(this.pGraphics, AntiAlias := 4) + Gdip_SetTextRenderingHint(this.pGraphics, AntiAlias := 4) + + ; Draw border + if (this.borderWidth > 0) { + pBrushBorder := Gdip_BrushCreateSolid(this.borderColor) + this.FillRoundedRect(this.pGraphics, pBrushBorder, 0, 0, this.previewWidth, this.previewHeight, this.cornerRadius) + Gdip_DeleteBrush(pBrushBorder) + } + + ; Draw background + pBrushBg := Gdip_BrushCreateSolid(this.backgroundColor) + bgX := this.borderWidth + bgY := this.borderWidth + bgW := this.previewWidth - this.borderWidth * 2 + bgH := this.previewHeight - this.borderWidth * 2 + bgCornerRadius := this.cornerRadius > this.borderWidth ? this.cornerRadius - this.borderWidth : 0 + this.FillRoundedRect(this.pGraphics, pBrushBg, bgX, bgY, bgW, bgH, bgCornerRadius) + Gdip_DeleteBrush(pBrushBg) + + ; Draw preedit + currentY := this.padding + this.borderWidth + prdSelTextRect := { x: this.padding + this.borderWidth, y: currentY, w: this.prdSelSize.w, h: this.prdSelSize.h } + prdHlTextRect := { x: this.padding * 2 + this.prdSelSize.w, y: currentY, w: this.prdHlSize.w, h: this.prdHlSize.h } + this.DrawText(this.pGraphics, "RIME", prdSelTextRect, this.textColor) + pBrsh_hlBg := Gdip_BrushCreateSolid(this.hlBgColor) + Gdip_FillRoundedRectangle(this.pGraphics, pBrsh_hlBg, prdHlTextRect.x, prdHlTextRect.y, prdHlTextRect.w, prdHlTextRect.h - 2, r := 2) + Gdip_DeleteBrush(pBrsh_hlBg) + this.DrawText(this.pGraphics, "shu ru fa", prdHlTextRect, this.hlTxtColor) + currentY += this.prdSelSize.h + this.lineSpacing + + ; Draw candidates + for i, candidate in candsArray { + textColor := this.candTxtColor + if (i == selIndex) { ; Draw highlight if selected + textColor := this.hlCandTxtColor + pBrsh_hlCandBg := Gdip_BrushCreateSolid(this.hlCandBgColor) + highlightX := this.borderWidth + this.padding / 2 + highlightY := currentY - this.lineSpacing / 2 + highlightW := this.previewWidth - this.borderWidth * 2 - this.padding + highlightH := this.candSize.h + this.lineSpacing + Gdip_FillRoundedRectangle(this.pGraphics, pBrsh_hlCandBg, highlightX, highlightY, highlightW, highlightH, r := 4) + Gdip_DeleteBrush(pBrsh_hlCandBg) + } + + textToDraw := i . ". " . candidate + candidateRowRect := { x: this.padding + this.borderWidth, y: currentY, w: this.maxRowWidth, h: this.candSize.h } + this.DrawText(this.pGraphics, textToDraw, candidateRowRect, textColor) + currentY += this.candSize.h + this.lineSpacing + } + + ; Replace preview image with hBitmap + this.hBitmap := Gdip_CreateHBITMAPFromBitmap(this.pBitmap) + SendMessage(STM_SETIMAGE := 0x0172, IMAGE_BITMAP := 0, this.hBitmap, this.imgCtrl.Hwnd) + + this.ReleaseDrawingSurface() + } + + ReleaseFont() { + if (this.hFont) { + Gdip_DeleteFont(this.hFont) + this.hFont := 0 + } + if (this.hFamily) { + Gdip_DeleteFontFamily(this.hFamily) + this.hFamily := 0 + } + if (this.hFormat) { + Gdip_DeleteStringFormat(this.hFormat) + this.hFormat := 0 + } + } + + ReleaseDrawingSurface() { + if (this.pGraphics) { + Gdip_DeleteGraphics(this.pGraphics) + this.pGraphics := 0 + } + if (this.pBitmap) { + Gdip_DisposeImage(this.pBitmap) + this.pBitmap := 0 + } + if (this.hBitmap) { + DeleteObject(this.hBitmap) + this.hBitmap := 0 + } + } + + ReleaseAll() { + this.ReleaseFont() + this.ReleaseDrawingSurface() + + if (this.pToken) { + Gdip_Shutdown(this.pToken) + this.pToken := 0 + } + } + + MeasureString(pGraphics, text, hFont, hFormat, &RectF) { + rc := Buffer(16) + ; !Notice, this way gets incorrect dim in test + ; dim := Gdip_MeasureString(pGraphics, text, hFont, hFormat, &rc) + ; rect := StrSplit(dim, "|") + ; return { w: Round(rect[3]), h: Round(rect[4]) } + + DllCall("gdiplus\GdipMeasureString", + "Ptr", pGraphics, + "WStr", text, + "Int", -1, + "Ptr", hFont, + "Ptr", RectF.Ptr, + "Ptr", hFormat, + "Ptr", rc.Ptr, + "UInt*", 0, + "UInt*", 0, + "Int") + + return { x: NumGet(rc.Ptr, 0, "Float"), y: NumGet(rc.Ptr, 4, "Float"), + w: NumGet(rc.Ptr, 8, "Float"), h: NumGet(rc.Ptr, 12, "Float") } + } + + DrawText(pGraphics, text, textRect, color) { + this.pBrush := Gdip_BrushCreateSolid(color) + CreateRectF(&RC, textRect.x, textRect.y, textRect.w, textRect.h) + Gdip_DrawString(pGraphics, text, this.hFont, this.hFormat, this.pBrush, &RC) + Gdip_DeleteBrush(this.pBrush) + } + + FillRoundedRect(pGraphics, pBrush, x, y, w, h, r) { + if (r <= 0) { + Gdip_FillRectangle(pGraphics, pBrush, x, y, w, h) + } else { + Gdip_FillRoundedRectangle(pGraphics, pBrush, x, y, w, h, r) + } + } +} + +class ThemesGUI { + __New(result) { + this.result := result + this.preset_color_schemes := Map() + this.colorSchemeMap := Map() + this.previewFontName := UIStyle.font_face + this.previewFontSize := UIStyle.font_point + this.themeListBoxW := 400 + this.previewGroupW := 300 + this.previewGroupH := 418 + this.previewGroupOffset := 20 + this.currentTheme := "aqua" + this.candsArray := ["输入法", "输入", "数", "书", "输"] + this.gui := Gui("+LastFound +OwnDialogs -DPIScale +AlwaysOnTop", "选择主题") + this.gui.MarginX := 10 + this.gui.MarginY := 10 + this.gui.SetFont("s10", "Microsoft YaHei UI") + this.Build() + } + + Build() { + this.preset_color_schemes := this.GetPresetStylesMap() + local colorChoices := [] + for key, preset in this.preset_color_schemes { + colorChoices.Push(preset["name"]) + this.colorSchemeMap[preset["name"]] := key + } + this.gui.Add("Text", "x10 y10", "主题:").GetPos(, , , &titleH) + this.titleH := titleH + + this.themeListBox := this.gui.AddListBox("r15 w" . this.themeListBoxW . " -Multi", colorChoices) + this.themeListBox.Choose(1) + this.themeListBox.OnEvent("Change", this.OnChangeColorScheme.Bind(this)) + this.gui.AddGroupBox(Format("x+{:d} yp-8 w{:d} h{:d} Section", this.previewGroupOffset, this.previewGroupW, this.previewGroupH), "预览") + ; 0xE(SS_BITMAP) or 0x4E (Bitmap and Resizable, but text is unclear) + this.previewImg := this.gui.AddPicture("xp+50 yp+50 w180 h300 0xE BackgroundWhite") + + this.currentTheme := this.colorSchemeMap[this.themeListBox.Text] + this.SetPreviewCandsBox(this.currentTheme, this.previewFontName, this.previewFontSize) + + this.setFontBtn := this.gui.AddButton("x10 ys+440 w160", "设置字体") + this.confirmBtn := this.gui.AddButton("x+400 ys+440 w160", "确定") + this.setFontBtn.OnEvent("Click", this.OnSetFont.Bind(this)) + this.confirmBtn.OnEvent("Click", this.OnConfirm.Bind(this)) + } + + Show() { + this.gui.Show("AutoSize") + } + + OnChangeColorScheme(ctrl, info) { + if !this.colorSchemeMap.Has(ctrl.Text) + return + + this.currentTheme := this.colorSchemeMap[ctrl.Text] + this.SetPreviewCandsBox(this.currentTheme, this.previewFontName, this.previewFontSize) + } + + OnSetFont(*) { + fontGui := Gui("AlwaysOnTop +Owner" this.gui.Hwnd, "字体选择") + fontGui.SetFont("s10") + + fontGui.AddText("x10 y10", "字体名称:") + fontChoice := fontGui.AddDropDownList("x+10 yp-4 w180 hp r10", GUIUtilities.GetFontArray()) + fontChoice.Text := this.previewFontName + + fontGui.AddText("x+30 y10", "大小:") + fontSizeEdit := fontGui.Add("Edit", "x+0 yp-6 w60 Limit2 Number") + fontGui.AddUpDown("Range10-20", this.previewFontSize) + + okBtn := fontGui.AddButton("x10 yp+30 w120", "确定") + fontGui.AddButton("x+150 yp w120", "取消").OnEvent("Click", (*) => fontGui.Destroy()) + + okBtn.OnEvent("Click", (*) => ( + this.previewFontName := fontChoice.Text, + this.previewFontSize := fontSizeEdit.Value, + this.SetPreviewCandsBox(this.currentTheme, this.previewFontName, this.previewFontSize), + fontGui.Destroy() + )) + + fontGui.Show() + } + + OnConfirm(*) { + global rime + if rime and config := rime.config_open("rabbit") { + rime.config_set_string(config, "style/color_scheme", this.currentTheme) + rime.config_set_int(config, "style/font_point", this.previewFontSize) + rime.config_set_string(config, "style/font_face", this.previewFontName) + UIStyle.Update(config, init := true) + rime.config_close(config) + this.result.yes := true + } + + this.gui.Hide() + } + + SetPreviewCandsBox(theme, fontName, fontSize) { + this.previewStyle := this.GetThemeColor(theme) + this.previewStyle.font_face := fontName + this.previewStyle.font_point := fontSize + candidateBox := CandidatePreview(this.previewImg, this.previewStyle, &candidateBoxW, &candidateBoxH) + previewCandsBoxX := this.gui.MarginX + this.themeListBoxW + this.previewGroupOffset + Round((this.previewGroupW - candidateBoxW) / 2) + previewCandsBoxY := this.gui.MarginY + this.titleH + Round((this.previewGroupH - candidateBoxH) / 2) + this.previewImg.Move(previewCandsBoxX, previewCandsBoxY, candidateBoxW, candidateBoxH) + candidateBox.Render(this.candsArray, 1) + } + + GetPresetStylesMap() { + local presetStylesMap := Map() + global rime + if rime and config := rime.config_open("rabbit") { + if iter := rime.config_begin_map(config, "preset_color_schemes") { + while rime.config_next(iter) { + styleMap := Map() + theme := StrLower(iter.key) + if name := rime.config_get_string(config, "preset_color_schemes/" . theme . "/name") { + styleMap["name"] := name + UIStyle.UpdateColor(config, theme) + } + styleMap["border_color"] := UIStyle.border_color + styleMap["text_color"] := UIStyle.text_color + styleMap["back_color"] := UIStyle.back_color + styleMap["hilited_text_color"] := UIStyle.hilited_text_color + styleMap["hilited_back_color"] := UIStyle.hilited_back_color + styleMap["hilited_candidate_text_color"] := UIStyle.hilited_candidate_text_color + styleMap["hilited_candidate_back_color"] := UIStyle.hilited_candidate_back_color + styleMap["candidate_text_color"] := UIStyle.candidate_text_color + styleMap["candidate_back_color"] := UIStyle.candidate_back_color + presetStylesMap[theme] := styleMap + } + rime.config_end(iter) + } + ; restore UIStyle + UIStyle.Update(config, init := true) + rime.config_close(config) + } + return presetStylesMap + } + + GetThemeColor(selTheme) { + style := this.preset_color_schemes[selTheme] + return { + border_color: style["border_color"], + text_color: style["text_color"], + back_color: style["back_color"], + hilited_text_color: style["hilited_text_color"], + hilited_back_color: style["hilited_back_color"], + hilited_candidate_text_color: style["hilited_candidate_text_color"], + hilited_candidate_back_color: style["hilited_candidate_back_color"], + candidate_text_color: style["candidate_text_color"], + candidate_back_color: style["candidate_back_color"], + } + } +} + +Class GUIUtilities { + static GetFontArray() { + static fontArr + if isSet(fontArr) + return fontArr + + sFont := Buffer(128, 0) + NumPut("UChar", 1, sFont, 23) + DllCall("EnumFontFamiliesEx", "ptr", DllCall("GetDC", "ptr", 0), "ptr", sFont.Ptr, "ptr", CallbackCreate(EnumFontProc), "ptr", ObjPtr(fontMap := Map()), "uint", 0) + + fontArr := Array() + for key, value in fontMap + fontArr.Push(SubStr(key, 2)) ; remove "@" + return fontArr + + EnumFontProc(lpFont, lpntme, textFont, lParam) { + font := StrGet(lpFont + 28, "UTF-16") + ObjFromPtrAddRef(lParam)[font] := "" + return true + } + } + + static GetMonitorDpiScale() { + hr := DllCall( + "Shcore.dll\GetDpiForMonitor", + "ptr", hMonitor := DllCall("MonitorFromPoint", "int64", 0, "uint", 2, "ptr"), + "int", MDT_EFFECTIVE_DPI := 0, + "uint*", &dpiX := 0, + "uint*", &dpiY := 0 + ) + + if (hr != 0) + return 1 + + return dpiX / 96 + } +} diff --git a/Lib/RabbitTrayMenu.ahk b/Lib/RabbitTrayMenu.ahk index 8af8e8b..e4fa913 100644 --- a/Lib/RabbitTrayMenu.ahk +++ b/Lib/RabbitTrayMenu.ahk @@ -84,9 +84,8 @@ RunDeployer(cmd, argv*) { ToggleSuspend() { global rime, session_id, box, STATUS_TOOLTIP - ToolTip() - if box - box.Show("Hide") + if box && HasMethod(box, "Hide") + box.Hide() rime.clear_composition(session_id) Suspend(-1) UpdateTrayTip() diff --git a/Lib/RabbitUIStyle.ahk b/Lib/RabbitUIStyle.ahk index 7777f8f..f0b05f4 100644 --- a/Lib/RabbitUIStyle.ahk +++ b/Lib/RabbitUIStyle.ahk @@ -20,10 +20,34 @@ class UIStyle { static use_dark := false - static text_color := 0xff000000 - static back_color := 0xffeceeee static font_face := "Microsoft YaHei UI" - static font_point := 12 + static label_font_face := "Microsoft YaHei UI" + static comment_font_face := "Microsoft YaHei UI" + static font_point := 14 + static label_font_point := 14 + static comment_font_point := 14 + static label_format := "{}. " + + static border_width := 2 + static corner_radius := 6 + static round_corner := 4 + static margin_x := 6 + static margin_y := 6 + static min_width := 160 + + static border_color := 0xffe0e0e0 + static text_color := 0xff000000 + static back_color := 0xffeeeeec + static candidate_text_color := 0xff000000 + static candidate_back_color := 0xffeeeeec + static label_color := 0xff000000 + static comment_text_color := 0xff000000 + static hilited_text_color := 0xff000000 + static hilited_back_color := 0xffd4d4d4 + static hilited_candidate_text_color := 0xffffffff + static hilited_candidate_back_color := 0xff0a3afa + static hilited_label_color := 0xffffffff + static hilited_comment_text_color := 0xff000000 static Update(config, initialize) { global rime @@ -33,9 +57,33 @@ class UIStyle { UIStyle.font_face := rime.config_get_string(config, "style/font_face") if not UIStyle.font_face UIStyle.font_face := "Microsoft YaHei UI" + UIStyle.label_font_face := rime.config_get_string(config, "style/label_font_face") + if not UIStyle.label_font_face + UIStyle.label_font_face := "Microsoft YaHei UI" + UIStyle.comment_font_face := rime.config_get_string(config, "style/comment_font_face") + if not UIStyle.comment_font_face + UIStyle.comment_font_face := "Microsoft YaHei UI" UIStyle.font_point := rime.config_get_int(config, "style/font_point") if UIStyle.font_point <= 0 - UIStyle.font_point := 12 + UIStyle.font_point := 14 + UIStyle.label_font_point := rime.config_get_int(config, "style/label_font_point") + if UIStyle.label_font_point <= 0 + UIStyle.label_font_point := 14 + UIStyle.comment_font_point := rime.config_get_int(config, "style/comment_font_point") + if UIStyle.comment_font_point <= 0 + UIStyle.comment_font_point := 14 + if rime.config_test_get_string(config, "style/label_format", &fmt) && fmt + UIStyle.label_format := fmt + if rime.config_test_get_int(config, "style/layout/corner_radius", &cr) && cr >= 0 + UIStyle.corner_radius := cr + if rime.config_test_get_int(config, "style/layout/round_corner", &r) && r >= 0 + UIStyle.round_corner := r + if rime.config_test_get_int(config, "style/layout/margin_x", &mx) && mx >= 0 + UIStyle.margin_x := mx + if rime.config_test_get_int(config, "style/layout/margin_y", &my) && my >= 0 + UIStyle.margin_y := my + if rime.config_test_get_int(config, "style/layout/min_width", &w) && w >= 0 + UIStyle.min_width := w if initialize and color := rime.config_get_string(config, "style/color_scheme") UIStyle.UpdateColor(config, color) } @@ -50,14 +98,49 @@ class UIStyle { fmt := cfmt } + UIStyle.border_color := UIStyle.GetColor(config, prefix . "/border_color", fmt, 0xffe0e0e0) UIStyle.text_color := UIStyle.GetColor(config, prefix . "/text_color", fmt, 0xff000000) UIStyle.back_color := UIStyle.GetColor(config, prefix . "/back_color", fmt, 0xffeceeee) + UIStyle.candidate_text_color := UIStyle.GetColor(config, prefix . "/candidate_text_color", fmt, UIStyle.text_color) + UIStyle.candidate_back_color := UIStyle.GetColor(config, prefix . "/candidate_back_color", fmt, UIStyle.back_color) + UIStyle.label_color := UIStyle.GetColor(config, prefix . "/label_color", fmt, UIStyle.BlendColors(UIStyle.candidate_text_color, UIStyle.candidate_back_color)) + UIStyle.comment_text_color := UIStyle.GetColor(config, prefix . "/comment_text_color", fmt, UIStyle.label_color) + UIStyle.hilited_text_color := UIStyle.GetColor(config, prefix . "/hilited_text_color", fmt, UIStyle.text_color) + UIStyle.hilited_back_color := UIStyle.GetColor(config, prefix . "/hilited_back_color", fmt, UIStyle.back_color) + UIStyle.hilited_candidate_text_color := UIStyle.GetColor(config, prefix . "/hilited_candidate_text_color", fmt, UIStyle.hilited_text_color) + UIStyle.hilited_candidate_back_color := UIStyle.GetColor(config, prefix . "/hilited_candidate_back_color", fmt, UIStyle.hilited_back_color) + UIStyle.hilited_label_color := UIStyle.GetColor(config, prefix . "/hilited_label_color", fmt, UIStyle.BlendColors(UIStyle.hilited_candidate_text_color, UIStyle.hilited_candidate_back_color)) + UIStyle.hilited_comment_text_color := UIStyle.GetColor(config, prefix . "/hilited_comment_text_color", fmt, UIStyle.hilited_label_color) return true } return false } + static BlendColors(fcolor, bcolor) { + local fA := (fcolor >> 24) & 0xff + if fA == 0xff + return fcolor + local fR := (fcolor >> 16) & 0xff + local fG := (fcolor >> 8) & 0xff + local fB := fcolor & 0xff + local bA := (bcolor >> 24) & 0xff + local bR := (bcolor >> 16) & 0xff + local bG := (bcolor >> 8) & 0xff + local bB := bcolor & 0xff + + local fAlpha := fA / 255.0 + local bAlpha := bA / 255.0 + + local retAlpha := fAlpha + bAlpha * (1 - fAlpha) + + local retR := Integer((fR * fAlpha + bR * bAlpha * (1 - fAlpha)) / retAlpha) + local retG := Integer((fG * fAlpha + bG * bAlpha * (1 - fAlpha)) / retAlpha) + local retB := Integer((fB * fAlpha + bB * bAlpha * (1 - fAlpha)) / retAlpha) + + return (Integer(retAlpha) * 255 << 24) | (retR << 16) | (retG << 8) | retB + } + static GetColor(config, key, fmt, fallback) { global rime if not rime.config_test_get_string(config, key, &color) @@ -143,5 +226,14 @@ OnColorChange(wParam, lParam, msg, hWnd) { rime.config_close(config) box.UpdateUIStyle() } + DarkMode.set(IS_DARK_MODE) + } +} + +; https://www.autohotkey.com/boards/viewtopic.php?p=515002&sid=859605067314b6d823a026658547b66f#p515002 +class DarkMode { + static set(mode) { + DllCall(DllCall("GetProcAddress", "ptr", DllCall("GetModuleHandle", "str", "uxtheme", "ptr"), "ptr", 135, "ptr"), "int", mode) + DllCall(DllCall("GetProcAddress", "ptr", DllCall("GetModuleHandle", "str", "uxtheme", "ptr"), "ptr", 136, "ptr")) } } diff --git a/Lib/librime-ahk b/Lib/librime-ahk index bd0f16f..501b506 160000 --- a/Lib/librime-ahk +++ b/Lib/librime-ahk @@ -1 +1 @@ -Subproject commit bd0f16ff98658a40bea219b5be24d92835689f06 +Subproject commit 501b50607c7d8223c216968c968b9dcbdf36af3b diff --git a/README.md b/README.md index 05c8592..02b39d0 100644 --- a/README.md +++ b/README.md @@ -6,32 +6,29 @@ [![Build Status](https://github.com/rimeinn/rabbit/actions/workflows/ci.yaml/badge.svg)](https://github.com/rimeinn/rabbit/actions/workflows/ci.yaml) [![Telegram Group Chat](https://telegram-badge.vercel.app/api/telegram-badge?channelId=@rime_rabbit)](https://t.me/rime_rabbit) [![License](https://img.shields.io/github/license/rimeinn/rabbit)](LICENSE) -![GitHub Repo stars](https://img.shields.io/github/stars/rimeinn/rabbit?style=flat) +[![GitHub Repo stars](https://img.shields.io/github/stars/rimeinn/rabbit?style=flat)](https://github.com/rimeinn/rabbit/stargazers) ## 下载体验 > [!NOTE] -> 发现程序漏洞请在 [Issues](https://github.com/rimeinn/rabbit/issues/new/choose) 反馈。 -> 使用问题可以在 [Discussions](https://github.com/rimeinn/rabbit/discussions) 讨论, -> 或者加入 [Telegram 群聊](https://t.me/rime_rabbit)。 +> 发现程序漏洞请在 [Issues](https://github.com/rimeinn/rabbit/issues/new/choose) 反馈。使用问题可以在 [Discussions](https://github.com/rimeinn/rabbit/discussions) 讨论,或者加入 [Telegram 群聊](https://t.me/rime_rabbit)。 -### Release 版 +### 通过发布页面下载 -发行版会在 [Release 页面](https://github.com/rimeinn/rabbit/releases) 的 Assets 中,下载最新的 `rabbit-v<版本号>.zip`,解压到一个新建文件夹,运行 `Rabbit.exe` 即可。 +正式发行版会在 [Release 页面](https://github.com/rimeinn/rabbit/releases) 的 Assets 中,下载最新的 `rabbit-v<版本号>.zip`,解压到一个新建文件夹,运行 `Rabbit.exe` 即可。 -#### 通过 [scoop](https://scoop.sh/) 安装 +每夜构建版可在 [`latest`](https://github.com/rimeinn/rabbit/releases/tag/latest) 页面下载。 + +### 通过 [scoop](https://scoop.sh/) 安装 ```PowerShell scoop bucket add siku https://github.com/amorphobia/siku +# 正式发行版 scoop install siku/rabbit +# 每夜构建版 +scoop install siku/rabbit-nightly ``` -### Action 版 - -需要先登录你的 GitHub 账号。 - -前往 [Actions 页面](https://github.com/rimeinn/rabbit/actions) 找到最近成功构建的一次,在生成的 Artifacts 中点击 `Rabbit-Full` 下载,将压缩包内容解压到一个新建目录中,运行 `Rabbit.exe` 即可。之后更新时,可只下载 `Rabbit` 或 `Data` 覆盖相应的文件。 - ## 脚本编译 本仓库提供*源码形式的玉兔毫脚本*以及*仅修改主图标的 AutoHotkey 可执行文件*,用户可根据需要自行编译为可执行文件以及压缩。编译方式可参照 AutoHotkey 的[官方文档](https://www.autohotkey.com/docs/v2/Scripts.htm#ahk2exe)。 @@ -40,6 +37,9 @@ scoop install siku/rabbit ## 目录结构 +
+点击展开 + > [!NOTE] > 以下描述的*可删除*、*编译后可删除*指的是删除后不影响使用,若要再次分发脚本或编译后的可执行文件,需遵守 [GPL-3.0 开源许可](LICENSE)。 @@ -61,14 +61,18 @@ rabbit/ ├─ rime-install.bat 东风破批处理脚本,删除后无法从设定中调用东风破 ``` +
+ ## 使用的开源项目 - [librime](https://github.com/rime/librime) - [OpenCC](https://github.com/BYVoid/OpenCC) +- [AHKv2-Gdip](https://github.com/buliasz/AHKv2-Gdip) - [librime-ahk](https://github.com/rimeinn/librime-ahk) - [GetCaretPos](https://github.com/Descolada/AHK-v2-libraries) - [GetCaretPosEx](https://github.com/Tebayaki/AutoHotkeyScripts/tree/main/lib/GetCaretPosEx) - [东风破](https://github.com/rime/plum) +- [小狼毫](https://github.com/rime/weasel) 以及一些代码片段,在注释中注明了来源链接 diff --git a/Rabbit.ahk b/Rabbit.ahk index 88ea554..92095d5 100644 --- a/Rabbit.ahk +++ b/Rabbit.ahk @@ -42,7 +42,7 @@ RabbitMain(A_Args) ; args[2]: deployer result ; args[3]: keyboard layout RabbitMain(args) { - global box + global box, rabbit_traits if args.Length >= 3 layout := Number(args[3]) if !IsSet(layout) || layout == 0 { @@ -100,7 +100,10 @@ RabbitMain(args) { CleanOldLogs() RabbitConfig.load() - box := CandidateBox() + if RabbitConfig.use_legacy_candidate_box + box := LegacyCandidateBox() + else + box := CandidateBox() RegisterHotKeys() UpdateStateLabels() if status := rime.get_status(session_id) { @@ -357,7 +360,7 @@ ProcessKey(key, mask, this_hotkey) { else last_is_hide := false SendText(commit.text) - box.Show("Hide") + box.Hide() rime.free_commit(commit) } else last_is_hide := false @@ -386,7 +389,7 @@ ProcessKey(key, mask, this_hotkey) { show_at_left_top := !!info if show_at_left_top && !last_is_hide { box.Build(context, &box_width, &box_height) - box.Show("AutoSize NA x" . info.work.left + 4 . " y" . info.work.top + 4) + box.Show(info.work.left + 4, info.work.top + 4) } } if !show_at_left_top && GetCaretPos(&caret_x, &caret_y, &caret_w, &caret_h) { @@ -416,7 +419,7 @@ ProcessKey(key, mask, this_hotkey) { } } if !last_is_hide - box.Show("AutoSize NA x" . new_x . " y" . new_y) + box.Show(new_x, new_y) prev_x := new_x prev_y := new_y } else if !show_at_left_top { @@ -425,11 +428,11 @@ ProcessKey(key, mask, this_hotkey) { MouseGetPos(&mouse_x, &mouse_y) CoordMode("Mouse", backup_mouse_ref) box.Build(context, &box_width, &box_height) - box.Show("AutoSize NA x" . mouse_x . " y" . mouse_y) + box.Show(mouse_x, mouse_y) } prev_show := true } else { - box.Show("Hide") + box.Hide() prev_show := false } rime.free_context(context) diff --git a/RabbitDeployer.ahk b/RabbitDeployer.ahk index d9dad39..24f01d7 100644 --- a/RabbitDeployer.ahk +++ b/RabbitDeployer.ahk @@ -24,6 +24,7 @@ #Include #Include +#Include ;@Ahk2Exe-SetMainIcon Lib\rabbit-alt.ico global IN_MAINTENANCE := true @@ -110,6 +111,27 @@ ConfigureSwitcher(levers, switcher_settings, &reconfigured) { return false } +ConfigureUI(levers, ui_style_settings, &reconfigured) { + if !IsSet(reconfigured) + reconfigured := false + local settings := ui_style_settings.settings + if !levers.load_settings(settings) + return false + result := { + yes : false + } + dialog := UIStyleSettingsDialog(ui_style_settings, result) + dialog.Show() + WinWaitClose(dialog) + + if result.yes { + if levers.save_settings(settings) + reconfigured := true + return true + } + return false +} + class Configurator extends Class { __New() { CreateFileIfNotExist("default.custom.yaml") @@ -117,6 +139,7 @@ class Configurator extends Class { } Initialize() { + global rabbit_traits rabbit_traits := CreateTraits() rime.setup(rabbit_traits) rime.deployer_initialize(0) @@ -128,11 +151,16 @@ class Configurator extends Class { return 1 switcher_settings := levers.switcher_settings_init() + ui_style_settings := UIStyleSettings() skip_switcher_settings := installing && !levers.is_first_run(switcher_settings) + skip_ui_style_settings := installing && !levers.is_first_run(ui_style_settings.settings) if !skip_switcher_settings { - ConfigureSwitcher(levers, switcher_settings, &reconfigured) + if !ConfigureSwitcher(levers, switcher_settings, &reconfigured) + skip_ui_style_settings := true ; user cancelled } + if !skip_ui_style_settings + ConfigureUI(levers, ui_style_settings, &reconfigured) levers.custom_settings_destroy(switcher_settings) @@ -368,6 +396,7 @@ class SwitcherSettingsDialog extends Gui { this.hotkeys := this.AddEdit("-Multi ReadOnly r1 w505") this.proxy_prompt := this.AddText("XS", "代理服务器:") this.proxy := this.AddEdit("X+10 -Multi r1 w300") + DllCall("SendMessage", "Ptr", this.proxy.Hwnd, "UInt", EM_SETCUEBANNER := 0x1501, "UPtr", true, "WStr", "如 http://127.0.0.1:7890", "Ptr") this.use_git := this.AddCheckbox("X+20", "使用 Git") this.use_git.Value := 1 this.more_schemas := this.AddButton("XS w155", "获取更多输入方案…") @@ -490,3 +519,146 @@ class SwitcherSettingsDialog extends Gui { this.Destroy() } } + +class UIStyleSettings { + __New() { + this.api := RimeLeversApi() + this.settings := this.api.custom_settings_init("rabbit", "Rabbit.UIStyleSettings") + } + + GetPresetColorSchemes() { + global rime + local result := [] + if !config := this.api.settings_get_config(this.settings) + return result + if !rime || !preset := rime.config_begin_map(config, "preset_color_schemes") + return result + while rime.config_next(preset) { + local name_key := preset.path . "/name" + if !name := rime.config_get_cstring(config, name_key) + continue + local author_key := preset.path . "/author" + local author := rime.config_get_cstring(config, author_key) + UIStyle.UpdateColor(config, StrLower(preset.key)) + result.Push({ + color_scheme_id: preset.key, + name: name, + author: author, + border_color: UIStyle.border_color, + text_color: UIStyle.text_color, + back_color: UIStyle.back_color, + hilited_text_color: UIStyle.hilited_text_color, + hilited_back_color: UIStyle.hilited_back_color, + hilited_candidate_text_color: UIStyle.hilited_candidate_text_color, + hilited_candidate_back_color: UIStyle.hilited_candidate_back_color, + candidate_text_color: UIStyle.candidate_text_color, + candidate_back_color: UIStyle.candidate_back_color, + font_face: UIStyle.font_face, + font_point: UIStyle.font_point, + }) + } + return result + } + + GetActiveColorScheme() { + global rime + if !config := this.api.settings_get_config(this.settings) + return "" + if !rime || !value := rime.config_get_cstring(config, "style/color_scheme") + return "" + return value + } + + SelectColorScheme(color_scheme_id) { + this.api.customize_string(this.settings, "style/color_scheme", color_scheme_id) + return true + } +} + +class UIStyleSettingsDialog extends Gui { + __New(settings, result) { + super.__New("-MaximizeBox -MinimizeBox", "【玉兔毫】界面风格设定", this) + this.settings := settings + this.loaded := false + this.api := RimeLeversApi() + + this.preset := [] + this.result := result + + ; Layout + this.MarginX := 15 + this.MarginY := 15 + this.color_schemes_width := 220 + this.preview_width := 220 + this.preview_offset := 20 + this.AddText("x10 y10", "主题:").GetPos(, , , &h) + this.title_height := h + this.color_schemes := this.AddListBox(Format("Section r15 w{} -Multi", this.color_schemes_width)) + this.color_schemes.OnEvent("Change", (ctrl, info) => this.OnColorSchemeSelChange()) + this.color_schemes.GetPos(, , , &h) + this.list_height := h + this.AddGroupBox(Format("x+{} yp-8 w{} h{}", this.preview_offset, this.preview_width, this.list_height + 8), "预览") + ; 0xE(SS_BITMAP) or 0x4E (Bitmap and Resizable, but text is unclear) + this.preview_img := this.AddPicture("xp+50 yp+50 w180 h180 0xE BackgroundWhite") + + this.set_font := this.AddButton(Format("xs ys+{} w120", this.list_height + this.MarginY), "设置字体") + this.set_font.Opt("+Disabled") ; TODO: implement font setting + this.ok := this.AddButton("x+180 w90", "中") + this.ok.OnEvent("Click", (*) => this.OnOK()) + + this.Populate() + } + + Populate() { + if !this.settings + return + local active := this.settings.GetActiveColorScheme() + local active_index := 0 + this.preset := this.settings.GetPresetColorSchemes() + local names := [] + for i, info in this.preset { + names.Push(info.name) + if info.color_scheme_id = active + active_index := i + } + this.color_schemes.Opt("-Redraw") + this.color_schemes.Add(names) + this.color_schemes.Opt("+Redraw") + if active_index > 0 { + this.color_schemes.Choose(active_index) + this.Preview(active_index) + } + this.loaded := true + } + + OnColorSchemeSelChange() { + local index := this.color_schemes.Value + if index > 0 && index <= this.preset.Length { + this.settings.SelectColorScheme(this.preset[index].color_scheme_id) + this.Preview(index) + } + return 0 + } + + Preview(index) { + if index <= 0 || index > this.preset.Length + return + local info := this.preset[index] + local candidate_box := CandidatePreview(this.preview_img, info, &box_width, &box_height) + box_width := box_width / candidate_box.dpiSacle + box_height := box_height / candidate_box.dpiSacle + local box_x := this.MarginX + this.color_schemes_width + this.preview_offset + Round((this.preview_width - box_width) / 2) + local box_y := this.MarginY + this.title_height + 8 + Round((this.list_height - box_height) / 2) + this.preview_img.Move(box_x, box_y, box_width, box_height) + candidate_box.Render(["输入法", "输入", "数", "书", "输"], 1) + } + + OnOK() { + this.Exit(true) + } + + Exit(yes) { + this.result.yes := yes + this.Destroy() + } +} diff --git a/schemas/rabbit.yaml b/schemas/rabbit.yaml index 9fea6fa..dd506db 100644 --- a/schemas/rabbit.yaml +++ b/schemas/rabbit.yaml @@ -26,12 +26,18 @@ fix_candidate_box: false style: color_scheme: aqua - font_point: 12 + font_face: Microsoft YaHei UI + label_font_face: Microsoft YaHei UI + comment_font_face: Microsoft YaHei UI + font_point: 14 + label_font_point: 14 + comment_font_point: 14 + # 不同于小狼毫,格式化标签文本需符合 AutoHotkey 语法 + # 详见 https://wyagd001.github.io/v2/docs/lib/Format.htm#FormatSpec + label_format: "{:s}. " # Copied from weasel.yaml -# Current supported fields -# - text_color -# - back_color +# 默认颜色格式为 argb,暂不支持 alpha 通道 preset_color_schemes: aqua: name: 碧水/Aqua