internal/ui: disable IME and enable it only when necessary

Closes #2918
This commit is contained in:
Hajime Hoshi 2024-08-26 23:26:58 +09:00
parent 5f595efef0
commit 42209606b1
7 changed files with 122 additions and 4 deletions

View File

@ -15,6 +15,7 @@
package textinput
import (
"errors"
"fmt"
"runtime"
"unsafe"
@ -73,6 +74,7 @@ var (
imm32 = windows.NewLazySystemDLL("imm32.dll")
user32 = windows.NewLazySystemDLL("user32.dll")
procImmAssociateContext = imm32.NewProc("ImmAssociateContext")
procImmGetCompositionStringW = imm32.NewProc("ImmGetCompositionStringW")
procImmGetContext = imm32.NewProc("ImmGetContext")
procImmReleaseContext = imm32.NewProc("ImmReleaseContext")
@ -94,6 +96,14 @@ func _GetActiveWindow() windows.HWND {
return windows.HWND(r)
}
func _ImmAssociateContext(hwnd windows.HWND, hIMC uintptr) (uintptr, error) {
r, _, e := procImmAssociateContext.Call(uintptr(hwnd), hIMC)
if e != nil && !errors.Is(e, windows.ERROR_SUCCESS) {
return 0, fmt.Errorf("textinput: ImmAssociateContext failed: error code: %w", e)
}
return r, nil
}
func _ImmGetCompositionStringW(unnamedParam1 _HIMC, unnamedParam2 uint32, lpBuf unsafe.Pointer, dwBufLen uint32) (uint32, error) {
r, _, e := procImmGetCompositionStringW.Call(uintptr(unnamedParam1), uintptr(unnamedParam2), uintptr(lpBuf), uintptr(dwBufLen))
runtime.KeepAlive(lpBuf)

View File

@ -15,6 +15,7 @@
package textinput
import (
"sync"
"unsafe"
"golang.org/x/sys/windows"
@ -29,8 +30,13 @@ type textInput struct {
origWndProc uintptr
wndProcCallback uintptr
window windows.HWND
immContext uintptr
highSurrogate uint16
initOnce sync.Once
err error
}
var theTextInput textInput
@ -52,11 +58,29 @@ func (t *textInput) Start(x, y int) (chan State, func()) {
session.ch <- State{Error: err}
session.end()
}
return session.ch, session.end
return session.ch, func() {
ui.Get().RunOnMainThread(func() {
// Disable IME again.
if t.immContext != 0 {
return
}
c, err := _ImmAssociateContext(t.window, 0)
if err != nil {
t.err = err
return
}
t.immContext = c
t.end()
})
}
}
// start must be called from the main thread.
func (t *textInput) start(x, y int) error {
if t.err != nil {
return t.err
}
if t.window == 0 {
t.window = _GetActiveWindow()
}
@ -72,6 +96,22 @@ func (t *textInput) start(x, y int) error {
t.origWndProc = h
}
// By default, IME was disabled by setting 0 as the IMM context.
// Restore the context once.
var err error
t.initOnce.Do(func() {
err = ui.Get().RestoreIMMContextOnMainThread()
})
if err != nil {
return err
}
if t.immContext != 0 {
if _, err := _ImmAssociateContext(t.window, t.immContext); err != nil {
return err
}
t.immContext = 0
}
h := _ImmGetContext(t.window)
if err := _ImmSetCandidateWindow(h, &_CANDIDATEFORM{
dwIndex: 0,
@ -168,14 +208,20 @@ func (t *textInput) send(text string, startInBytes, endInBytes int, committed bo
// end must be called from the main thread.
func (t *textInput) end() {
if t.session != nil {
t.session.end()
t.session = nil
if t.session == nil {
return
}
t.session.end()
t.session = nil
}
// update must be called from the main thread.
func (t *textInput) update() (ferr error) {
if t.err != nil {
return t.err
}
hIMC := _ImmGetContext(t.window)
defer func() {
if err := _ImmReleaseContext(t.window, hIMC); err != nil && ferr != nil {
@ -236,6 +282,10 @@ func (t *textInput) update() (ferr error) {
// commit must be called from the main thread.
func (t *textInput) commit() (ferr error) {
if t.err != nil {
return t.err
}
hIMC := _ImmGetContext(t.window)
defer func() {
if err := _ImmReleaseContext(t.window, hIMC); err != nil && ferr != nil {

View File

@ -68,9 +68,12 @@ type _POINT struct {
}
var (
imm32 = windows.NewLazySystemDLL("imm32.dll")
ole32 = windows.NewLazySystemDLL("ole32.dll")
user32 = windows.NewLazySystemDLL("user32.dll")
procImmAssociateContext = imm32.NewProc("ImmAssociateContext")
procCoCreateInstance = ole32.NewProc("CoCreateInstance")
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
@ -79,6 +82,14 @@ var (
procGetCursorPos = user32.NewProc("GetCursorPos")
)
func _ImmAssociateContext(hwnd windows.HWND, hIMC uintptr) (uintptr, error) {
r, _, e := procImmAssociateContext.Call(uintptr(hwnd), hIMC)
if e != nil && !errors.Is(e, windows.ERROR_SUCCESS) {
return 0, fmt.Errorf("ui: ImmAssociateContext failed: error code: %w", e)
}
return r, nil
}
func _CoCreateInstance(rclsid *windows.GUID, pUnkOuter unsafe.Pointer, dwClsContext uint32, riid *windows.GUID) (unsafe.Pointer, error) {
var ptr unsafe.Pointer
r, _, _ := procCoCreateInstance.Call(uintptr(unsafe.Pointer(rclsid)), uintptr(pUnkOuter), uintptr(dwClsContext), uintptr(unsafe.Pointer(riid)), uintptr(unsafe.Pointer(&ptr)))

View File

@ -444,3 +444,7 @@ func (u *UserInterface) setDocumentEdited(edited bool) error {
objc.ID(w).Send(sel_setDocumentEdited, edited)
return nil
}
func (u *UserInterface) afterWindowCreation() error {
return nil
}

View File

@ -106,6 +106,9 @@ type userInterfaceImpl struct {
showWindowOnce sync.Once
bufferOnceSwappedOnce sync.Once
// immContext is used only in Windows.
immContext uintptr
m sync.RWMutex
}
@ -897,6 +900,10 @@ func (u *UserInterface) createWindow() error {
return err
}
if err := u.afterWindowCreation(); err != nil {
return err
}
return nil
}

View File

@ -204,3 +204,7 @@ func (u *UserInterface) skipTaskbar() error {
func (u *UserInterface) setDocumentEdited(edited bool) error {
return nil
}
func (u *UserInterface) afterWindowCreation() error {
return nil
}

View File

@ -246,6 +246,38 @@ func (u *UserInterface) setDocumentEdited(edited bool) error {
return nil
}
func (u *UserInterface) afterWindowCreation() error {
if microsoftgdk.IsXbox() {
return nil
}
// By default, IME should be disabled (#2918).
w, err := u.window.GetWin32Window()
if err != nil {
return err
}
c, err := _ImmAssociateContext(w, 0)
if err != nil {
return err
}
u.immContext = c
return nil
}
// RestoreIMMContextOnMainThread is called from the main thread.
// The textinput package invokes RestoreIMMContextOnMainThread to enable IME inputting.
func (u *UserInterface) RestoreIMMContextOnMainThread() error {
w, err := u.window.GetWin32Window()
if err != nil {
return err
}
if _, err := _ImmAssociateContext(w, u.immContext); err != nil {
return err
}
u.immContext = 0
return nil
}
func init() {
if microsoftgdk.IsXbox() {
// TimeBeginPeriod might not be defined in Xbox.