[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