[LyX/master] Add support for IM attribute specification over multiple segments
Koji Yokota
yokota at lyx.org
Sun Jun 14 05:04:06 UTC 2026
commit f198a3847c685e29565ffb952caf7870ba45bc60
Author: Koji Yokota <yokota at lyx.org>
Date: Sun Jun 14 13:57:45 2026 +0900
Add support for IM attribute specification over multiple segments
Although QInputMethodEvent documents that there should be at most one
text format attribute specified on each segment, the de-facto standard
seems different. LyX allowed duplicate attribute specification segment
by segment so far. This fix extends it to allow overwrapped
specification over segments.
Eg. existing attribute [ seg ] [charFormat for a segment] [seg]
incoming attribute [ <------ charFormat over segments ------> ]
---
src/frontends/qt/GuiInputMethod.cpp | 192 ++++++++++++++++++++++++++++--------
src/frontends/qt/GuiInputMethod.h | 3 +-
src/frontends/qt/GuiPainter.cpp | 4 +-
3 files changed, 155 insertions(+), 44 deletions(-)
diff --git a/src/frontends/qt/GuiInputMethod.cpp b/src/frontends/qt/GuiInputMethod.cpp
index e6b026fd97..7d073483c4 100644
--- a/src/frontends/qt/GuiInputMethod.cpp
+++ b/src/frontends/qt/GuiInputMethod.cpp
@@ -460,12 +460,11 @@ void GuiInputMethod::setPreeditStyle(
} // end for
// Finalize TextFormat: sweep all remaining turnouts
- for (size_type i=0; i<d->seg_turnout_.size(); ++i)
- next_seg_pos = pickNextSegFromTurnout(next_seg_pos);
- if (!d->seg_turnout_.empty()) {
- LYXERR0("Turnouts of preedit segments have not been all swept");
- LATTEST(false);
+ for (size_type i=0; i< d->seg_turnout_.size(); ++i) {
+ d->style_.segments_.push_back(d->seg_turnout_.back());
+ d->seg_turnout_.pop_back();
}
+ LASSERT(d->seg_turnout_.empty(), d->seg_turnout_.clear());
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// set background color for a focused segment
@@ -559,13 +558,18 @@ pos_type GuiInputMethod::setTextFormat(const QInputMethodEvent::Attribute & it,
// it.length == 0, whose info we don't use, so it does not go in if's below
if (it.start == next_seg_pos && !d->initial_tf_entry_) {
if (!d->seg_turnout_.empty()) {
- // Merge attributes held d->seg_turnout_
+ // Merge attributes held in d->seg_turnout_
pos_type updated_pos =
- pickNextSegFromTurnout(next_seg_pos, &char_format);
+ pickNextSegFromTurnout(it.start, it.length, &char_format);
if (updated_pos == next_seg_pos) {
// no matching segment in the turnout
- LYXERR(Debug::KEY, "Pushing to preedit register: [" << it.start
- << ", " << it.start + it.length << ") bg color: "
+ LYXERR(Debug::KEY, "Mixed with turnout and registered: ["
+ << it.start << ", " << it.start + it.length
+ << ") underline? " << char_format.fontUnderline()
+ << " UnderlineStyle: " << char_format.underlineStyle()
+ << " fg: "
+ << char_format.foreground().color().name(QColor::HexArgb)
+ << ") bg color: "
<< char_format.background().color().name(QColor::HexArgb));
next_seg_pos = registerSegment(it.start, (size_type)it.length,
char_format);
@@ -574,77 +578,181 @@ pos_type GuiInputMethod::setTextFormat(const QInputMethodEvent::Attribute & it,
} else {
// push the constructed char format together with start and length
// to the list
- LYXERR(Debug::KEY, "Pushing to preedit register: [" << it.start
- << ", " << it.start + it.length
- << ") fg: "
+ LYXERR(Debug::KEY, "Registered: [" << it.start << ", "
+ << it.start + it.length << ") underline? "
+ << char_format.fontUnderline() << " UnderlineStyle: "
+ << char_format.underlineStyle() << " fg: "
<< char_format.foreground().color().name(QColor::HexArgb)
<< " bg: "
<< char_format.background().color().name(QColor::HexArgb));
next_seg_pos =
registerSegment(it.start, (size_type)it.length, char_format);
}
- next_seg_pos = pickNextSegFromTurnout(next_seg_pos);
} else if ((it.start > next_seg_pos || d->initial_tf_entry_) && it.length > 0) {
- LYXERR(Debug::KEY, "Pushing to preedit turnout: [" << it.start << ", "
- << it.start + it.length << ")");
+ LYXERR(Debug::KEY, "Pushing to preedit turnout: [" << it.start <<
+ ", " << it.start + it.length << ") underline? "
+ << char_format.fontUnderline() << " UnderlineStyle: "
+ << char_format.underlineStyle() << " fg: "
+ << char_format.foreground().color().name(QColor::HexArgb)
+ << " bg: "
+ << char_format.background().color().name(QColor::HexArgb));
PreeditSegment turnout = {it.start, (size_type)it.length, char_format};
d->seg_turnout_.push_back(turnout);
d->initial_tf_entry_ = false;
- }
+ } else // it.length == 0 or it.start < next_seg_pos
+ LYXERR(Debug::KEY, "Ignored: [" << it.start <<
+ ", " << it.start + it.length << ") underline? "
+ << char_format.fontUnderline() << " UnderlineStyle: "
+ << char_format.underlineStyle() << " fg: "
+ << char_format.foreground().color().name(QColor::HexArgb)
+ << " bg: "
+ << char_format.background().color().name(QColor::HexArgb));
return next_seg_pos;
}
-pos_type GuiInputMethod::pickNextSegFromTurnout(pos_type next_seg_pos,
- QTextCharFormat * cf)
+pos_type GuiInputMethod::pickNextSegFromTurnout(pos_type next_seg_pos, size_type length,
+ QTextCharFormat * cf)
{
- std::vector<PreeditSegment>::iterator to_erase;
+ std::vector<PreeditSegment> to_erase;
+ std::vector<PreeditSegment> carryover;
bool is_matched = false;
+ LYXERR(Debug::KEY, "Pick from turnout: turn_out size = " <<
+ d->seg_turnout_.size());
+
// we prepare "multiple tracks" in d->seg_turnout_,
// but typically only one is used
for (auto past_attr = d->seg_turnout_.begin();
past_attr != d->seg_turnout_.end(); past_attr++) {
- if (past_attr->start_ == next_seg_pos) {
+
+ // UNDOCUMENTED: a segment should not have multiple specifications
+ // according to the QInputMethodEvent documentation (Qt6),
+ // but it's commonly observed and seems a defacto standard
+ //
+ // * we refrain from calling a sort function since original segments
+ // arrive mostly in order
+ //
+ // we take into account two patterns of relative segment positions
+ // between new incoming [A, B] and already arrived [C, D] (though the
+ // latter may not be necessary):
+ //
+ // A [---------------------) B new incoming segment
+ // C [------) D segment in turnout
+ //
+ // or
+ //
+ // A [--------------) B new incoming segment
+ // ^ C [----------------) D segment in turnout
+ // | | |
+ // | [---------)
+ // next_seg_pos |-> carried over for next incoming segment
+ //
+ // next_seg_pos steps up to the next segment position (in order) once
+ // the corresponding segment arrives.
+ // Note that in most of the cases [C, D) indicates a focused segment,
+ // which arrives first. The arrival order after that is not guaranteed.
+ // Segments that arrive after next_seg_pos has swept that interval will
+ // be simply ignored (this pattern is not observed).
+ //
+ pos_type & A = next_seg_pos;
+ pos_type B = next_seg_pos + length;
+ pos_type & C = past_attr->start_;
+ pos_type D = past_attr->start_ + past_attr->length_;
+ //
+ // note: always A <= C < B if overwraps
+
+ QTextCharFormat & past_cf = past_attr->char_format_;
+
+ // case: segment given in args and past_attr have common elements
+ if (A <= C && C < B) {
PreeditSegment seg;
- if (cf != nullptr) {
- cf->merge(past_attr->char_format_);
- seg = {past_attr->start_,
- (size_type)past_attr->length_, *cf};
- } else
- seg = {past_attr->start_,
- (size_type)past_attr->length_,
- past_attr->char_format_};
+
+ // 1. leading unshared elements if exist ( [A, C) )
+ if (A < C) {
+ seg = {A, (size_type)(C - A), *cf};
+ LYXERR(Debug::KEY,
+ "Registered: [" << A << ", " << C << ") underline? "
+ << cf->fontUnderline() << " UnderlineStyle: "
+ << cf->underlineStyle() << " fg: "
+ << cf->foreground().color().name(QColor::HexArgb)
+ << " bg: "
+ << cf->background().color().name(QColor::HexArgb));
+ d->style_.segments_.push_back(seg);
+ }
+
+ // 2. shared elements ( [C, min(B, D)) )
+ size_type seg_len = min(D - C, B - C);
+ // note: *cf has changed here
+ if (cf != nullptr)
+ cf->merge(past_cf);
+ else
+ *cf = past_cf;
+ seg = {C, seg_len, *cf};
LYXERR(Debug::KEY,
- "Pushing to preedit register: [" << past_attr->start_
- << ", " << past_attr->start_ + past_attr->length_
- << ") fg: "
- << past_attr->char_format_.foreground().color().name(QColor::HexArgb)
- << " bg: "
- << past_attr->char_format_.background().color().name(QColor::HexArgb));
+ "Merged and registered: [" << C << ", " << C + seg_len
+ << ") underline? " << cf->fontUnderline()
+ << " UnderlineStyle: " << cf->underlineStyle() << " fg: "
+ << cf->foreground().color().name(QColor::HexArgb)
+ << " bg: "
+ << cf->background().color().name(QColor::HexArgb));
d->style_.segments_.push_back(seg);
- next_seg_pos += past_attr->length_;
+
+ // 3. trailing unshared elements if exist ( [D, B) if D < B )
+ if (past_attr->start_ + past_attr->length_ <
+ next_seg_pos + length) {
+ seg = {D, (size_type)(B - D), *cf};
+ LYXERR(Debug::KEY,
+ "Registered: [" << D << ", " << B << ") underline? "
+ << cf->fontUnderline() << " UnderlineStyle: "
+ << cf->underlineStyle() << ") fg: "
+ << past_cf.foreground().color().name(QColor::HexArgb)
+ << " bg: "
+ << past_cf.background().color().name(QColor::HexArgb));
+ d->style_.segments_.push_back(seg);
+ }
+
+ // 4. remainder ( [B, D) if D > B )
+ // There's no observed case for this yet (2026/6/13)
+ if (D > B) {
+ carryover.push_back({B, (size_type)(D - B), past_cf});
+ LYXERR(Debug::KEY,
+ "Pushing back to turnout: [" << B << ", " << D
+ << ") underline? " << cf->fontUnderline()
+ << " UnderlineStyle: " << cf->underlineStyle() << ") fg: "
+ << past_cf.foreground().color().name(QColor::HexArgb)
+ << " bg: "
+ << past_cf.background().color().name(QColor::HexArgb));
+ }
+
+ next_seg_pos += length;
if (d->seg_turnout_.size() > 1)
- to_erase = past_attr;
+ to_erase.push_back(*past_attr);
is_matched = true;
break; // assuming no duplicates in seg_turnout_
}
}
+
// Clear d->seg_turnout_
if (is_matched) {
if (d->seg_turnout_.size() == 1) {
- LYXERR(Debug::KEY, "Preedit turnout clearing: ["
+ LYXERR(Debug::KEY, "Removing from preedit turnout: ["
<< d->seg_turnout_.back().start_ << ", "
<< d->seg_turnout_.back().start_
+ d->seg_turnout_.back().length_ << ")");
d->seg_turnout_.pop_back();
} else if (d->seg_turnout_.size() > 1) {
- LYXERR(Debug::KEY, "Preedit turnout clearing: ["
- << (*to_erase).start_ << ", "
- << (*to_erase).start_ + (*to_erase).length_ << ")");
- d->seg_turnout_.erase(to_erase);
+ for (auto it = to_erase.begin(); it != to_erase.end(); it++) {
+ LYXERR(Debug::KEY, "Removing from preedit turnout: ["
+ << it->start_ << ", "
+ << it->start_ + it->length_ << ")");
+ d->seg_turnout_.erase(it);
+ }
}
}
+ // Append the remainder to d->seg_turnout_
+ d->seg_turnout_.insert(d->seg_turnout_.end(),
+ carryover.begin(), carryover.end());
return next_seg_pos;
}
@@ -715,6 +823,8 @@ void GuiInputMethod::setCaretRectangle()
{
d->caret_elem_ = setCaretPreeditElement();
int x,y;
+ LASSERT(d->caret_elem_.index < (pos_type)d->elems_coords_.size(), return);
+
if (d->im_state_.composing_mode_) {
x = d->elems_coords_[d->caret_elem_.index][0] + preeditCaretOffset();
y = d->elems_coords_[d->caret_elem_.index][1];
diff --git a/src/frontends/qt/GuiInputMethod.h b/src/frontends/qt/GuiInputMethod.h
index fe56b8a6c6..e843701170 100644
--- a/src/frontends/qt/GuiInputMethod.h
+++ b/src/frontends/qt/GuiInputMethod.h
@@ -178,7 +178,8 @@ private:
/// Pick up next segment from the turnout if there is a match and return
/// the next segment position to be filled
/// If the second argument is given, it is merged before filling the segment
- pos_type pickNextSegFromTurnout(pos_type next_seg_pos, QTextCharFormat * char_format = nullptr);
+ pos_type pickNextSegFromTurnout(pos_type next_seg_pos, size_type length,
+ QTextCharFormat * char_format = nullptr);
/// Register preedit segment for final output
pos_type registerSegment(pos_type start, size_type length, QTextCharFormat char_format);
/// Returns enum Qt::InputMethodQuery constant from its value
diff --git a/src/frontends/qt/GuiPainter.cpp b/src/frontends/qt/GuiPainter.cpp
index 00ce3205c3..40389399c2 100644
--- a/src/frontends/qt/GuiPainter.cpp
+++ b/src/frontends/qt/GuiPainter.cpp
@@ -393,9 +393,9 @@ void GuiPainter::text(int x, int y, docstring const & s,
LYXERR(Debug::GUI, "Drawing preedit segment " << char_format_index <<
": fg = " <<
- gim->charFormat(char_format_index).foreground().color().name() <<
+ gim->charFormat(char_format_index).foreground().color().name(QColor::HexArgb) <<
" bg = " <<
- gim->charFormat(char_format_index).background().color().name());
+ gim->charFormat(char_format_index).background().color().name(QColor::HexArgb));
drawText(x, y, str);
setBackgroundMode(Qt::TransparentMode);
More information about the lyx-cvs
mailing list