diff --git a/components/synedit/lazsynimmbase.pas b/components/synedit/lazsynimmbase.pas index 7972e464bc..11cab771cd 100644 --- a/components/synedit/lazsynimmbase.pas +++ b/components/synedit/lazsynimmbase.pas @@ -30,6 +30,14 @@ type procedure WMImeComposition(var Msg: TMessage); virtual; procedure WMImeStartComposition(var Msg: TMessage); virtual; procedure WMImeEndComposition(var Msg: TMessage); virtual; + // JPSupport (Qt5/Qt6): the candidate-window caret-position query and + // the preedit (composing) text notification. These mirror the + // structure of the existing WMIme* messages above; see LazSynImeQt + // (lazsynqtimm.pas) for the concrete implementation. Kept virtual and + // empty by default so other platforms' LazSynIme subclasses are + // unaffected. + procedure WMImeQueryCaretPos(var Msg: TMessage); virtual; + procedure WMImeSetPreedit(var Msg: TMessage); virtual; procedure FocusKilled; virtual; property InvalidateLinesMethod : TInvalidateLines write FInvalidateLinesMethod; property OnIMEStart: TNotifyEvent read FOnIMEStart write FOnIMEStart; @@ -92,6 +100,12 @@ end; procedure LazSynIme.WMImeEndComposition(var Msg: TMessage); begin end; +procedure LazSynIme.WMImeQueryCaretPos(var Msg: TMessage); +begin +end; +procedure LazSynIme.WMImeSetPreedit(var Msg: TMessage); +begin +end; procedure LazSynIme.FocusKilled; begin diff --git a/components/synedit/lazsynqtimm.pas b/components/synedit/lazsynqtimm.pas new file mode 100644 index 0000000000..667b43a2d3 --- /dev/null +++ b/components/synedit/lazsynqtimm.pas @@ -0,0 +1,191 @@ +unit LazSynQtIMM; +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, Types, Graphics, Controls, ExtCtrls, LCLType, LMessages, + Messages, LazSynIMMBase, SynEditMiscClasses, SynEditTypes; + +type + { LazSynImeQt } + LazSynImeQt = class(LazSynIme) + private + FPreeditBox: TPaintBox; + FPreeditInfo: TIMEPreeditInfo; + procedure EnsurePreeditBox; + procedure PreeditBoxPaint(Sender: TObject); + public + procedure WMImeQueryCaretPos(var Msg: TMessage); override; + procedure WMImeSetPreedit(var Msg: TMessage); override; + end; + +implementation + +{ LazSynImeQt } + +procedure LazSynImeQt.WMImeQueryCaretPos(var Msg: TMessage); +var + R: PRect; +begin + R := PRect(Msg.LParam); + if R = nil then + Exit; + R^.Left := FriendEdit.CaretXPix; + R^.Top := FriendEdit.CaretYPix; + R^.Right := R^.Left + 2; + R^.Bottom := R^.Top + FriendEdit.LineHeight; + Msg.Result := 1; +end; + +procedure LazSynImeQt.EnsurePreeditBox; +begin + if FPreeditBox <> nil then + Exit; + FPreeditBox := TPaintBox.Create(FriendEdit); + FPreeditBox.Parent := TWinControl(FriendEdit); + FPreeditBox.Visible := False; + FPreeditBox.OnPaint := @PreeditBoxPaint; +end; + +procedure LazSynImeQt.PreeditBoxPaint(Sender: TObject); +var + TextH: Integer; + i, SegStart, SegLen, SegLeft, SegW: Integer; + SegText: WideString; + IsFocused: Boolean; + CursorX: Integer; +const + // JPSupport (Qt5/Qt6): fallback highlight color for the currently- + // focused bunsetsu (segment), used since Fcitx5/Mozc's own TextFormat + // background is read but a consistent, theme-independent highlight is + // preferable to whatever raw color Qt reports. + clJPSupportFocusedSegment = $00D8B000; // BGR: a muted (pale) blue highlight + // A more saturated blue than the pale highlight above, so the thick + // underline is visually distinct from the highlight fill it sits on + // top of (same color for both would make the underline invisible). + clJPSupportFocusedUnderline = $00E08000; // BGR: a stronger, saturated blue +begin + with FPreeditBox.Canvas do + begin + Font := FriendEdit.Font; + TextH := TextHeight('Wg'); + Brush.Color := FriendEdit.Color; + Brush.Style := bsSolid; + FillRect(ClipRect); + SegLeft := 0; + if FPreeditInfo.SegmentCount = 0 then + begin + // No segment attributes at all (e.g. still in raw romaji-typing + // stage, before Fcitx5/Mozc has started segmenting) - draw the + // whole preedit string as one plain, underlined run. + TextOut(0, 0, FPreeditInfo.Text); + Pen.Color := Font.Color; + Line(0, TextH - 1, TextWidth(FPreeditInfo.Text), TextH - 1); + end + else + begin + for i := 0 to FPreeditInfo.SegmentCount - 1 do + begin + SegStart := FPreeditInfo.Segments[i].Start; + SegLen := FPreeditInfo.Segments[i].Length; + IsFocused := FPreeditInfo.Segments[i].Focused; + if (SegStart < 0) or (SegLen <= 0) or + (SegStart + SegLen > Length(FPreeditInfo.Text)) then + Continue; + SegText := Copy(FPreeditInfo.Text, SegStart + 1, SegLen); + Font.Style := FriendEdit.Font.Style; + SegW := TextWidth(SegText); + if IsFocused then + begin + Brush.Color := clJPSupportFocusedSegment; + Brush.Style := bsSolid; + // Leave the bottom few pixels of the highlight unfilled, so the + // thick underline drawn below (in a more saturated color) + // remains visible instead of blending into a same-toned fill. + FillRect(Rect(SegLeft, 0, SegLeft + SegW, TextH - 3)); + end; + Font.Color := FriendEdit.Font.Color; + Brush.Style := bsClear; + TextOut(SegLeft, 0, SegText); + // JPSupport: underline for the currently-focused segment - drawn + // as a filled rectangle (not a thick Pen line) so the extra + // thickness extends only downward from the baseline instead of + // bleeding symmetrically upward into the text, and in a + // saturated accent color - matching the bold, clearly-offset, + // colored underline convention seen in native GTK IME rendering + // (e.g. Gedit + Fcitx5/Mozc). Other segments keep the plain, + // thin, text-colored underline right at the baseline. + if IsFocused then + begin + Brush.Color := clJPSupportFocusedUnderline; + Brush.Style := bsSolid; + FillRect(Rect(SegLeft, TextH - 2, SegLeft + SegW, TextH + 1)); + Brush.Style := bsClear; + end + else + begin + Pen.Color := FriendEdit.Font.Color; + Pen.Width := 1; + Line(SegLeft, TextH - 1, SegLeft + SegW, TextH - 1); + end; + Inc(SegLeft, SegW); + end; + end; + // JPSupport: cursor position within the composing (preedit) text, + // reported by Fcitx5/Mozc via Qt's Cursor attribute - moves as the + // user adjusts bunsetsu (segment) boundaries with Shift+Left/Right. + // A plain, slightly-thick line in the text color is enough since + // SynEdit's own caret is hidden while composing (see WMImeSetPreedit) + // and no longer visually competes with it. + if FPreeditInfo.CursorVisible and (FPreeditInfo.CursorPos >= 0) and + (FPreeditInfo.CursorPos <= Length(FPreeditInfo.Text)) then + begin + CursorX := TextWidth(Copy(FPreeditInfo.Text, 1, FPreeditInfo.CursorPos)); + Pen.Color := Font.Color; + Pen.Width := 2; + Line(CursorX, 0, CursorX, TextH); + Pen.Width := 1; + end; + end; +end; + +procedure LazSynImeQt.WMImeSetPreedit(var Msg: TMessage); +var + Info: PIMEPreeditInfo; + TextW, TextH: Integer; +begin + Info := PIMEPreeditInfo(Msg.LParam); + if Info = nil then + Exit; + EnsurePreeditBox; + FPreeditInfo := Info^; + if Length(FPreeditInfo.Text) = 0 then + begin + FPreeditBox.Visible := False; + // JPSupport: composition ended - restore SynEdit's own (blinking) + // caret, which we hid while composing (see below). + ScreenCaret.Visible := not (eoNoCaret in FriendEdit.Options); + end + else + begin + // JPSupport: while composing, hide SynEdit's real caret. Otherwise it + // keeps blinking, unmoving, at the position where composition + // started, which looks like "the cursor is stuck" even though our + // own preedit-internal cursor line (above) correctly follows + // bunsetsu/segment navigation - the two carets were simply competing + // for attention, and the real one (blinking) is far more prominent. + ScreenCaret.Visible := False; + FPreeditBox.Canvas.Font := FriendEdit.Font; + TextW := FPreeditBox.Canvas.TextWidth(FPreeditInfo.Text); + TextH := FPreeditBox.Canvas.TextHeight(FPreeditInfo.Text); + // +2 extra pixels of height to fit the focused-segment underline, + // which is drawn a couple pixels below the text baseline. + FPreeditBox.SetBounds(FriendEdit.CaretXPix, FriendEdit.CaretYPix, TextW + 2, TextH + 3); + FPreeditBox.Visible := True; + FPreeditBox.Invalidate; + end; + Msg.Result := 1; // acknowledge: this control handles preedit itself +end; + +end. diff --git a/components/synedit/synedit.pp b/components/synedit/synedit.pp index 9a35faaf8d..2d526a0f5f 100644 --- a/components/synedit/synedit.pp +++ b/components/synedit/synedit.pp @@ -54,6 +54,17 @@ unit SynEdit; {$DEFINE CocoaIME} {$ENDIF} +// JPSupport (Qt5/Qt6): unlike GTK2 (whose Gtk2IME define was never +// wired up here at all - a known, long-standing gap), Qt5 and Qt6 +// share an (almost) identical qtwidgets.pas implementation, so a +// single LazSynImeQt handler class serves both widgetsets. +{$IFDEF LCLQt5} + {$DEFINE QtIME} +{$ENDIF} +{$IFDEF LCLQt6} + {$DEFINE QtIME} +{$ENDIF} + {$I synedit.inc} @@ -116,6 +127,9 @@ uses {$IFDEF Gtk2IME} LazSynGtk2IMM, {$ENDIF} + {$IFDEF QtIME} + LazSynQtIMM, + {$ENDIF} {$IFDEF USE_UTF8BIDI_LCL} FreeBIDI, utf8bidi, {$ENDIF} @@ -936,6 +950,20 @@ type // Caret function CaretXPix: Integer; override; function CaretYPix: Integer; override; + // JPSupport (Qt5): answers a caret/cursor-rectangle query sent by + // TQtWidget.SlotInputMethodQuery (lcl/interfaces/qt5/qtwidgets.pas). + // TQtWidget is a generic LCL-layer class that must not depend on the + // SynEdit package, so it cannot cast LCLObject to TCustomSynEdit and + // read CaretXPix/CaretYPix directly; instead it dispatches this + // message to whatever control has focus, and this handler (present + // only on TCustomSynEdit) fills in the TRect passed via Message.LParam. + procedure WMImeQueryCaretPos(var Message: TMessage); message LM_IM_QUERY_CARET_POS; + // JPSupport (Qt5): receives the current IME preedit (composition, + // with bunsetsu/segment and cursor info) from TQtWidget.SlotInputMethod, + // so this control can render it itself (see LM_IM_SET_PREEDIT comment + // in lmessages.pp for why this is necessary for a custom-painted + // control like TSynEdit). + procedure WMImeSetPreedit(var Message: TMessage); message LM_IM_SET_PREEDIT; procedure EnsureCursorPosVisible; procedure MoveCaretToVisibleArea; procedure MoveCaretIgnoreEOL(const NewCaret: TPoint); @@ -2402,6 +2430,10 @@ begin FImeHandler := LazSynImeGtk2 .Create(Self); FImeHandler.InvalidateLinesMethod := @InvalidateLines; {$ENDIF} + {$IFDEF QtIME} + FImeHandler := LazSynImeQt.Create(Self); + FImeHandler.InvalidateLinesMethod := @InvalidateLines; + {$ENDIF} fFontDummy.Name := SynDefaultFontName; fFontDummy.Height := SynDefaultFontHeight; @@ -2813,6 +2845,22 @@ begin Result := ScreenXYToPixels(p).Y; end; +procedure TCustomSynEdit.WMImeQueryCaretPos(var Message: TMessage); +begin + // JPSupport (Qt5/Qt6): thin forwarder to the IME handler class, matching + // the pattern already used for WMImeComposition etc. on other platforms + // (e.g. GTK2's GTK_IMComposition). All the actual logic lives in + // LazSynImeQt (lazsynqtimm.pas). + if FImeHandler <> nil then + FImeHandler.WMImeQueryCaretPos(Message); +end; + +procedure TCustomSynEdit.WMImeSetPreedit(var Message: TMessage); +begin + if FImeHandler <> nil then + FImeHandler.WMImeSetPreedit(Message); +end; + procedure TCustomSynEdit.FontChanged(Sender: TObject); begin // TODO: inherited ? FPaintArea.ForegroundColor := Font.Color;