Skip to content

Commit ada474d

Browse files
committed
fix(tip): override IMM32 property to enable commit on unselect
1 parent 8bbc052 commit ada474d

File tree

7 files changed

+99
-17
lines changed

7 files changed

+99
-17
lines changed

editor/src-tauri/Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

preferences/src-tauri/Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tip/src/imm32.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! Undocumented IMM32 API.
2+
3+
pub(crate) const IME_PROP_COMPLETE_ON_UNSELECT: u32 = 0x00100000;
4+
5+
use windows::Win32::{Foundation::HINSTANCE, UI::Input::KeyboardAndMouse::HKL};
6+
7+
// Retrieve the pointer to an IME instance
8+
//
9+
// See https://github.com/reactos/reactos/blob/80bd4608363683131411f131e8783749e51835c5/win32ss/user/imm32/ime.c#L531
10+
windows_core::link!("imm32.dll" "system" fn ImmLockImeDpi(hkl: HKL) -> *mut ImeDpi);
11+
12+
// Return the pointer of an IME instance
13+
//
14+
// See https://github.com/reactos/reactos/blob/80bd4608363683131411f131e8783749e51835c5/win32ss/user/imm32/ime.c#L561
15+
windows_core::link!("imm32.dll" "system" fn ImmUnlockImeDpi(pimedpi: *mut ImeDpi));
16+
17+
// IME info
18+
//
19+
// See https://github.com/reactos/reactos/blob/80bd4608363683131411f131e8783749e51835c5/sdk/include/ddk/immdev.h#L20
20+
#[repr(C)]
21+
pub(crate) struct ImeInfo {
22+
pub(crate) dw_private_data_size: u32,
23+
pub(crate) fdw_property: u32,
24+
pub(crate) fdw_conversion_caps: u32,
25+
pub(crate) fdw_sentence_caps: u32,
26+
pub(crate) fdw_uicaps: u32,
27+
pub(crate) fdw_scscaps: u32,
28+
pub(crate) fdw_select_caps: u32,
29+
}
30+
31+
// IME instance struct
32+
//
33+
// Unused remaining fields are not included.
34+
//
35+
// See https://github.com/reactos/reactos/blob/80bd4608363683131411f131e8783749e51835c5/sdk/include/reactos/imm32_undoc.h#L91
36+
#[repr(C)]
37+
pub(crate) struct ImeDpi {
38+
pnext: *const ImeDpi,
39+
hinst: HINSTANCE,
40+
hkl: HKL,
41+
pub(crate) ime_info: ImeInfo,
42+
}

tip/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
mod com;
44
pub mod config;
5+
mod imm32;
56
mod keybind;
67
mod logging;
78
mod text_service;

tip/src/text_service/chewing.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -776,16 +776,30 @@ impl ChewingTextService {
776776
ecwrite: u32,
777777
composition: &ITfComposition,
778778
) -> Result<()> {
779-
// commit current preedit
780779
unsafe {
780+
let composition_range = composition
781+
.GetRange()
782+
.inspect_err(|_| debug!("failed to get composition range"))?;
781783
let doc_mgr = self
782784
.thread_mgr
783785
.GetFocus()
784786
.context("failed to get current ITfDocumentMgr")?;
785787
let context = doc_mgr
786788
.GetTop()
787789
.context("failed to get current ITfContext")?;
788-
EndComposition::will_end_composition(&context, composition, ecwrite)?;
790+
791+
// When a composition is interrupted by the application we only need to
792+
// clear the display attributes. When I tested this, clearing the display attribute
793+
// is not necessary, but this is what mozc was doing.
794+
//
795+
// In pure TSF mode, the composition string is automatically committed on termination.
796+
// In TSF/IMM32 bridge mode, the bridge sets a default property which we override to
797+
// have the same commit on unselect behavior. See [`TextService_Impl::Activate`].
798+
let disp_attr_prop =
799+
context.GetProperty(&windows::Win32::UI::TextServices::GUID_PROP_ATTRIBUTE)?;
800+
disp_attr_prop
801+
.Clear(ecwrite, &composition_range)
802+
.inspect_err(|_| debug!("failed to clear display attribute"))?;
789803
}
790804
self.on_composition_terminated_tail()
791805
}

tip/src/text_service/edit_session.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,18 +171,18 @@ impl EndComposition {
171171
ec: u32,
172172
) -> Result<()> {
173173
unsafe {
174-
let range = composition
174+
let composition_range = composition
175175
.GetRange()
176176
.inspect_err(|_| debug!("failed to get composition range"))?;
177177
let disp_attr_prop = context.GetProperty(&GUID_PROP_ATTRIBUTE)?;
178178
disp_attr_prop
179-
.Clear(ec, &range)
179+
.Clear(ec, &composition_range)
180180
.inspect_err(|_| debug!("failed to clear display attribute"))?;
181181

182-
let new_composition_start = range.Clone()?;
183-
new_composition_start.Collapse(ec, TF_ANCHOR_END)?;
184-
composition.ShiftStart(ec, &new_composition_start)?;
185-
set_selection(context, ec, new_composition_start, TF_AE_END)?;
182+
composition_range.Collapse(ec, TF_ANCHOR_END)?;
183+
composition.ShiftStart(ec, &composition_range)?;
184+
composition.ShiftEnd(ec, &composition_range)?;
185+
set_selection(context, ec, composition_range, TF_AE_END)?;
186186
}
187187
Ok(())
188188
}

tip/src/text_service/mod.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
22

3-
use std::cell::{Cell, RefCell};
3+
use std::{
4+
cell::{Cell, RefCell},
5+
ptr::null_mut,
6+
};
47

58
use log::{debug, error};
69
use windows::Win32::{
710
Foundation::{E_UNEXPECTED, FALSE, LPARAM, WPARAM},
8-
UI::TextServices::*,
11+
UI::{Input::KeyboardAndMouse::GetKeyboardLayout, TextServices::*},
912
};
1013
use windows_core::{
1114
BOOL, BSTR, ComObjectInterface, GUID, IUnknown, IUnknown_Vtbl, Interface, InterfaceRef, Ref,
1215
Result, implement, interface,
1316
};
1417

15-
use crate::text_service::chewing::ReentrantOps;
18+
use crate::{
19+
imm32::{IME_PROP_COMPLETE_ON_UNSELECT, ImeDpi, ImmLockImeDpi, ImmUnlockImeDpi},
20+
text_service::chewing::ReentrantOps,
21+
};
1622

1723
use self::chewing::ChewingTextService;
1824
use self::display_attribute::{EnumTfDisplayAttributeInfo, get_display_attribute_info};
@@ -62,6 +68,7 @@ pub(super) struct TextService {
6268
keyboard_openclose_cookie: Cell<u32>,
6369
key_busy: Cell<bool>,
6470
composition_terminated: Cell<bool>,
71+
pimedpi: Cell<*mut ImeDpi>,
6572
}
6673

6774
impl TextService {
@@ -73,6 +80,7 @@ impl TextService {
7380
keyboard_openclose_cookie: Cell::new(TF_INVALID_COOKIE),
7481
key_busy: Cell::new(false),
7582
composition_terminated: Cell::new(false),
83+
pimedpi: Cell::new(null_mut()),
7684
}
7785
}
7886
fn tail_handler(&self) {
@@ -143,6 +151,18 @@ impl IFnRunCommand_Impl for TextService_Impl {
143151
impl ITfTextInputProcessor_Impl for TextService_Impl {
144152
fn Activate(&self, ptim: Ref<ITfThreadMgr>, tid: u32) -> Result<()> {
145153
debug!(tid; "tip::activate");
154+
155+
debug!("trying to override the default IMM32 property set by MSCTF.dll");
156+
let hkl = unsafe { GetKeyboardLayout(0) };
157+
let pimedpi = unsafe { ImmLockImeDpi(hkl) };
158+
if let Some(imedpi) = unsafe { pimedpi.as_mut() } {
159+
imedpi.ime_info.fdw_property |= IME_PROP_COMPLETE_ON_UNSELECT;
160+
debug!("done adding IME_PROP_COMPLETE_ON_UNSELECT to IME property");
161+
} else {
162+
debug!("unable to get the PIMEDPI pointer");
163+
}
164+
self.pimedpi.set(pimedpi);
165+
146166
self.tid.set(tid);
147167
let mut ts = self.inner.borrow_mut();
148168
let mut thread_cookies = self.thread_cookies.borrow_mut();
@@ -186,6 +206,11 @@ impl ITfTextInputProcessor_Impl for TextService_Impl {
186206

187207
fn Deactivate(&self) -> Result<()> {
188208
debug!("tip::deactivate");
209+
if !self.pimedpi.get().is_null() {
210+
debug!("releasing previously acquired PIMEDPI pointer");
211+
unsafe { ImmUnlockImeDpi(self.pimedpi.get()) };
212+
self.pimedpi.set(null_mut());
213+
}
189214
let thread_cookies = self.thread_cookies.take();
190215

191216
if let Some(ts) = self.inner.borrow_mut().take() {

0 commit comments

Comments
 (0)