diff --git a/exp/textinput/api_windows.go b/exp/textinput/api_windows.go index 7c8617a7e..d7fae6299 100644 --- a/exp/textinput/api_windows.go +++ b/exp/textinput/api_windows.go @@ -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) diff --git a/exp/textinput/textinput_windows.go b/exp/textinput/textinput_windows.go index 982b02a0c..bc1b4b5d5 100644 --- a/exp/textinput/textinput_windows.go +++ b/exp/textinput/textinput_windows.go @@ -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 { diff --git a/internal/ui/api_windows.go b/internal/ui/api_windows.go index 160114953..6a5199910 100644 --- a/internal/ui/api_windows.go +++ b/internal/ui/api_windows.go @@ -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))) diff --git a/internal/ui/ui_darwin.go b/internal/ui/ui_darwin.go index ed4ff8fe6..6c73bcfc4 100644 --- a/internal/ui/ui_darwin.go +++ b/internal/ui/ui_darwin.go @@ -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 +} diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index 28dc21df3..9c3ccf342 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -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 } diff --git a/internal/ui/ui_linbsd.go b/internal/ui/ui_linbsd.go index 7d07f2bc3..4d8d79149 100644 --- a/internal/ui/ui_linbsd.go +++ b/internal/ui/ui_linbsd.go @@ -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 +} diff --git a/internal/ui/ui_windows.go b/internal/ui/ui_windows.go index b9bdf9ef0..77882e3a8 100644 --- a/internal/ui/ui_windows.go +++ b/internal/ui/ui_windows.go @@ -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.