Author Topic: [Qt5/Qt6] JPSupport-Qt: Japanese (CJK) IME support for SynEdit - seeking feedbac  (Read 1678 times)

shortcut

  • New Member
  • *
  • Posts: 10
Hello everyone,

Some of you may know JPSupport, a package I released a while back that
adds Japanese input method (IME) support to SynEdit on the GTK2
widgetset:
https://github.com/53jouhikone-source/JPSupport

Since GTK2 is end-of-life and GTK3 currently lags behind on IME
support, I've started porting the same kind of support to Qt5 and Qt6,
using Qt's standard input method API (QInputMethodEvent /
QInputMethodQueryEvent) instead of GTK2-specific hacks. The result is
JPSupport-Qt:
https://github.com/53jouhikone-source/JPSupport/tree/main/JPSupport-Qt

Unlike the GTK2 version (a standalone package), this one requires
patches to LCL core (lmessages.pp, components/synedit) and the Qt5/Qt6
interface layers, plus small C++ extensions to the bundled libQt5Pas/
libQt6Pas binding libraries - QInputMethodEvent::attributes() (needed
for bunsetsu/segment info) wasn't exposed at all upstream.

What's implemented and tested (on Fcitx5 + Mozc, across Raspberry Pi
4/5 and x86_64/Ubuntu):
- Fixed a commit-string truncation bug (UTF-16 was being misread as
  UTF-8, dropping multi-character CJK input)
- Both IME toggle keys work (Ctrl+Space, Zenkaku-Hankaku)
- Candidate window follows the cursor instead of a fixed position
- Preedit (composing) text is actually shown - previously nothing was
  rendered at all for SynEdit
- Bunsetsu (segment) highlighting and cursor tracking within the
  composing text

IBus + Mozc also works, with a couple of known limitations documented
in the repo (Ctrl+Space conflicts with the IDE's own Code Completion
shortcut; the candidate window doesn't follow the cursor, which looks
like a limitation on IBus's own Qt integration side).

The repo includes both a patch script (for applying directly to a
fixes_4 checkout) and ready-to-run Docker environments for trying it
without touching an existing installation.

I'd love to hear the team's thoughts on whether this would be a
reasonable candidate for upstreaming, and if so, what the right next
step would be (a GitLab issue, a merge request, or something else).
Happy to answer any questions about the implementation.

Thanks for reading!

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12721
  • Debugger - SynEdit - and more
    • wiki
I can only answer half of it.

I don't maintain the QT part. I did call it to attention, so hopefully an answer will follow... But otherwise I don't see why it shouldn't be added.

On SynEdit, I am happy to include the full code necessary to make it work.

On the LCL, if there are additional WM*/LM* messages needed I don't see why not => but they would need to be reviewed.


----
As for SynEdit, I like to keep the concept of SynEdit only forwarding any LM messages to the syn-IME handler class. That class (including its base class) can be extended for any new methods/signals needed.

If that class needs further access to the core SynEdit, that can be added into the code (either new (or elevated) methods in SynEdit, or callbacks)


shortcut

  • New Member
  • *
  • Posts: 10
Thank you very much for the quick and encouraging reply, and for
flagging the Qt part to the right people!

Regarding the SynEdit design concept: that makes a lot of sense, and I
completely understand the preference for keeping SynEdit itself as a
thin forwarder to the syn-IME handler class. My current implementation
(prototyped and tested, not yet a merge request) actually adds the new
message handling directly onto TCustomSynEdit rather than going through
LazSynIme - so it doesn't yet follow that pattern.

I'd be happy to rework it so that:
- The new WM_IME_QUERY_CARET_POS / WM_IME_SET_PREEDIT-style messages are
  forwarded to the LazSynIme handler class (extending the base class
  with the new methods/signals needed), rather than handled inline in
  TCustomSynEdit.
- Any additional access the handler needs (caret position, segment/
  bunsetsu info for the preedit overlay) is exposed through new or
  elevated SynEdit methods, or callbacks, as you suggested.

Since the GTK2 version of JPSupport already uses a LazSynIme subclass
(LazSynImeGtk2), I think this refactor should be quite natural to do
for Qt5/Qt6 as well - I'll take a look and come back with an updated
proposal once I've reworked it along these lines.

Thanks again - looking forward to hearing from the Qt side as well.

zeljko

  • Hero Member
  • *****
  • Posts: 2004
    • http://wiki.lazarus.freepascal.org/User:Zeljan
I've looked into your changes and I've spotted changes to the Qt C bindings. That is something I'd like to avoid if possible, since any change to the Qt bindings went into many opened issues and forum complains since it won't link because many ppl don't know howto update C bindings.

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12721
  • Debugger - SynEdit - and more
    • wiki
- Any additional access the handler needs (caret position, segment/
  bunsetsu info for the preedit overlay) is exposed through new or
  elevated SynEdit methods, or callbacks, as you suggested.

"overlay" => you may not need?
(ok / after having written the below / googled the term "bunsetsu" / should have done first => if overlay refers to drawing lines, boxes, .... see markup below )

At least the IME on Windows can directly display the temp-text in the editor. That is it actually modifies the current line. See LazSynImeFull.SetImeTempText


It blocks undo info, so it must restore any changes that it did, before either cancel, or inserting the final text (with undo info). Otherwise the SynEdit undo system would malfunction afterwards.


It uses
Code: Pascal  [Select][+][-]
  1. FImeMarkupSelection, FImeMarkupSelection2, FImeMarkupSelection3: TSynEditMarkupSelection;
to add highlights. E.g. the underline of the current substring that is active. (for dropdown, or whatever the IME does)

The dropdowns are drawn by the OS (at least on windows), so here only a pos is needed. But if you need to display candidate lists yourself, you can probably find ways to do that.

If you do test, test with word wrap too => to check you have the caret pos correct, if you return pixel pos to the OS for the OS to display stuff (e.g. drowdown).

You may already know, but read https://wiki.freepascal.org/SynEdit#Logical,_Physical_or_Viewed_caret_position

shortcut

  • New Member
  • *
  • Posts: 10
Thanks for taking a look, zeljko, and for being upfront about the
concern - that history with the C bindings makes complete sense to me,
and I'd genuinely like to avoid repeating it.

To explain the background: the segment (bunsetsu) highlighting and
cursor-tracking-within-composing-text feature relies on reading
QInputMethodEvent::attributes() - Qt's own mechanism for IMEs to convey
which part of the preedit text is currently focused, and where the
cursor sits within it. As far as I could find, this wasn't exposed
anywhere in the existing libQt5Pas/libQt6Pas bindings at all, which is
why I ended up adding new C++ accessor functions for it.

A few questions, if I may:
- Is there an existing mechanism I might have missed for reading
  QInputMethodEvent's attributes from Pascal, without touching the C
  bindings?
- If new C-binding functions really are the only way, is there a
  pattern you'd prefer for introducing them safely - e.g. something
  that fails gracefully (feature simply unavailable) rather than a
  hard link error, for people building against an older libQt5Pas?
- Alternatively, would it be acceptable to gate this specific feature
  (segment highlighting) behind a check for the new binding functions,
  so the rest of the patch (commit-string fix, cursor-following
  candidate window, preedit display) could go in independently, without
  requiring the C-binding change at all?

I'm also open to simply dropping the segment-highlighting feature if
that's the cleanest path forward - the core IME support (which doesn't
need any C-binding changes) is the more important part anyway.

Thanks again for the guidance.

dbannon

  • Hero Member
  • *****
  • Posts: 3889
    • tomboy-ng, a rewrite of the classic Tomboy
I've looked into your changes and I've spotted changes to the Qt C bindings. That is something I'd like to avoid if possible, since any change to the Qt bindings went into many opened issues and forum complains since it won't link because many ppl don't know howto update C bindings.

Zeljko, as someone who talks to the end user, please let me say that we really, really appreciate how stable the binding have been recently, its been great ! But targeted, purposeful changes are a good thing. Lots of people use CJK characters, the rest of use can stay where we are. The big distros will catch up !

If it turns out to be necessary, please do it.

Davo
Lazarus 4, Linux (and reluctantly Win10/11, OSX Monterey)
My Project - https://github.com/tomboy-notes/tomboy-ng and my github - https://github.com/davidbannon

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12721
  • Debugger - SynEdit - and more
    • wiki
For those of us not fluent in Japanese / and/or not regularly using an IME:
https://support.microsoft.com/en-us/windows/hardware/input-devices/microsoft-japanese-ime

According to google: "bunsetsu" = "phrasal unit"

And about the IME, in layman's terms, just from the experimenting I did on Windows...

When you type into the IME, a temporary sequence of glyphs is added. This sequence is "grouped", you can navigate left/right and select any of the groups of glyphs. For the selected group you can then choose alternative sets of glyphs "candidates". (So my guess Pressing the keys "add" adds "add" but could also have meant "dad" which you can then select. You don't have to select immediately, you can first continue to type).

There are different levels of implementing an IME client side.

1) Nothing, the OS will guess (and sometimes go top corner of the screen) the current cursor pos, and then do "2".

2) Just return the cursor pos.
The OS paints over the control, hiding the current text/content. When done the final text will be sent, either via API, or iirc even as sequence of virtual utf8-keystrokes.

3) Paint the temp text yourself.
This allows to shift other text, as if the temp text would be the final text in the editor.

---
When using the  3rd, I don't recall what the options (must implement, can leave to OS) are on things like highlighting the current group (actually there are 2 highlights, but don't recall the diff).

And then dropdowns for the alternative candidates must be displayed. This can be done by the app too, but can be left to the OS. (and probably has little benefit to be done by the app / font maybe?)

Anytime something is left to the OS, the OS needs info, at least about positions and sizes. Maybe more...




So depending on what info QT wants/needs it may need to communicate this. Usually that goes via messages (WM/LM) being sent. Its possible that not all of QT's needs are covered by the set provided by Windows.

Using those messages allows other components to benefit. E.g. the richedit component could then implement something too.

shortcut

  • New Member
  • *
  • Posts: 10
Update on the LazSynIme subclass refactor

Following Martin_fr's suggestion, I've refactored the Qt5/Qt6 IME implementation away from direct methods on TCustomSynEdit and into a proper LazSynIme subclass, LazSynImeQt (in a new unit, lazsynqtimm.pas), mirroring the existing pattern used by LazSynImeGtk2 and the other platform handlers.

Summary of the changes:

LazSynIme (in lazsynimmbase.pas) gains two new virtual methods, WMImeQueryCaretPos and WMImeSetPreedit, empty by default so other platforms are unaffected.
LazSynImeQt implements both, along with the preedit rendering (segment/bunsetsu highlighting, cursor tracking) that previously lived directly on TCustomSynEdit.
synedit.pp now only holds thin forwarders to FImeHandler, and creates a LazSynImeQt instance under a new QtIME define (set for both LCLQt5 and LCLQt6).

I've built and tested this on a clean Lazarus checkout (x86_64, Ubuntu 22.04, Qt5, Fcitx5+Mozc) and confirmed all previously-verified features still work correctly: commit processing, both conversion key types, candidate window tracking, preedit display, clause highlighting, and clause movement.

The patch script in the repository has been updated to match this design. Qt6 verification is still pending on my end, but the shared code path should apply equally to both.

Thanks again for the direction — happy to adjust further if this isn't quite what you had in mind.

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12721
  • Debugger - SynEdit - and more
    • wiki
I haven't heard from Zeljko yet.

About the patches, I don't know how much point there is for me to review the SynEdit point before we get confirmation on the QT part.
But I don't have Python installed, and reading them inline is really inconvenient. When/If I should look through them, I would really prefer either: a normal patch OR a patched file (and the branch/sha1 to which the patch was applied OR a forked branch with the patch applied.

shortcut

  • New Member
  • *
  • Posts: 10
Thanks for the feedback — here's a plain patch file, generated with git diff between the base commit and the patched commit (both against fixes_4):

https://raw.githubusercontent.com/53jouhikone-source/JPSupport/main/JPSupport-Qt/patches/upstream/jpsupport-qt-lazsynime-refactor.patch

  • Base: f379582c5f (origin/fixes_4 parent)
  • Patched: aa0befe860

This covers the SynEdit-side changes only (lazsynimmbase.pas, the new lazsynqtimm.pas, and synedit.pp) — no Qt5/Qt6 C++ binding changes are included, since those are separate and still pending Zeljko's input. Happy to provide it in a different format if this isn't convenient either.

shortcut

  • New Member
  • *
  • Posts: 10
Investigating alternatives to the C++ binding extension

While waiting for Zeljko's input, I wanted to make sure I wasn't missing an obvious way to avoid touching libQt5Pas/libQt6Pas at all, so I looked into a few alternatives in more depth (including a D-Bus-based approach that would bypass Qt's IME event system entirely, talking directly to fcitx5's own D-Bus interface).

Summary of what I found:

  • A separate D-Bus connection would need to either create its own competing InputContext (real risk of focus/key-handling conflicts with the one fcitx5-qt already owns), or eavesdrop on the existing one's signals — which isn't possible, since fcitx5's per-IC signals are unicast to the owning connection, not broadcast.
  • Going through Qt's own QInputMethodEvent (which is what the current patch already does) unfortunately can't be done from pure Pascal either: QInputMethodEvent::Attribute is a plain, non-QObject C++ struct, and the formatting data inside it is a QVariant-wrapped QTextCharFormat — both require C++-side code to unpack safely (no stable ABI to read them as raw pointers from Pascal, and no generic QVariant-to-arbitrary-type marshaling already in libQt5Pas).

So it looks like some minimal new C++ code is genuinely unavoidable if we want real per-segment formatting info (as opposed to a rough approximation reconstructed from raw key sequences, which wouldn't reflect fcitx5/Mozc's actual clause boundaries).

Given that, I'm raising this here as an open question rather than a conclusion — is there an approach I'm missing? And if the minimal C++ extraction function really is unavoidable, would placing it in a separate, optional shared library (rather than modifying libQt5Pas/libQt6Pas core) address the underlying concern? Happy to sketch out exactly what that would look like if it helps move the discussion forward.
« Last Edit: August 02, 2026, 10:57:06 pm by shortcut »

szlbz

  • Jr. Member
  • **
  • Posts: 56
When compiling, Lazarus reports that LM_IM_QUERY_CARET_POS and LM_IM_SET_PREEDIT are not defined.


shortcut

  • New Member
  • *
  • Posts: 10
Thanks for testing this, and sorry for the confusion — the plain patch file (jpsupport-qt-lazsynime-refactor.patch) I posted for Martin_fr's review has a hidden assumption I should have called out: it's a diff between two already-patched states of the tree, not something meant to apply to a clean Lazarus checkout on its own. It doesn't touch lmessages.pp, because at that point in the branch's history lmessages.pp had already been patched separately — which is exactly why LM_IM_QUERY_CARET_POS and LM_IM_SET_PREEDIT come back undefined if you apply just that file to a clean tree.

For actually trying this out, please use the full patch script instead, which applies everything (including lmessages.pp) in the right order:

python3 patches/apply_jpsupport_patches.py qt5

(or qt6, or both)

from a clean fixes_4 checkout. That standalone .patch file was only meant to give Martin_fr a readable diff of the SynEdit-side refactor for review — not a step users should apply on its own. I'll clarify this in the repo. Let me know if you still hit issues after using the script.

shortcut

  • New Member
  • *
  • Posts: 10
A process question, since it's been about three weeks with no response from Zeljko on the C++ binding question

I don't want to come across as pushing for an answer — I know maintainers have limited time, and I'd rather wait properly than nag. But I did some digging through the old mailing list archives to understand how this is normally handled, and found Martin_fr's own explanation from a 2019 thread: patches touching a specific area generally need sign-off from "the committer of that patch" / the responsible team member for that area, and without that, "it would sit there forever." So it sounds like there isn't a formal process for someone else stepping in on Zeljko's behalf for the Qt bindings specifically — which makes sense, and I don't want to route around that.

Given that, the realistic options as I see them are:

Keep waiting — this is still a reasonable timeframe, no action needed yet
Try reaching Zeljko through a different channel (GitLab issue/MR directly, rather than a forum thread)
Scope the change down to the absolute minimum and resubmit for a fresh look, in case the size/shape of the original ask was the sticking point rather than the concept itself

No urgency on my end — happy to keep working on other parts (Qt6 verification, etc.) in the meantime regardless. Just wanted to check whether I'm missing an established norm here before deciding how long "reasonable" is.

 

TinyPortal © 2005-2018