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

Reply via email to