[LyX/master] Add support for IM attribute specification over multiple segments
Koji Yokota <[email protected]> Sun, 14 Jun 2026 05:04:06 +0000
| Newsgroups | gmane.editors.lyx.cvs |
|---|---|
| Message-ID | <[email protected]> |
commit f198a3847c685e29565ffb952caf7870ba45bc60 Author: Koji Yokota <[email protected]> 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); -- lyx-cvs mailing list [email protected] https://lists.lyx.org/mailman/listinfo/lyx-cvs