commit 29d41c1e1bcde742a2a2e34c350aced78cb2df07
Author: Koji Yokota <[email protected]>
Date:   Thu Jul 3 11:21:03 2025 +0900

    Fix preedit representation on Windows/Linux and optimize
---
 src/frontends/qt/GuiInputMethod.cpp | 233 ++++++++++++++++++++++++++----------
 src/frontends/qt/GuiInputMethod.h   |  18 +--
 src/frontends/qt/GuiPainter.cpp     |   2 +
 3 files changed, 180 insertions(+), 73 deletions(-)

diff --git a/src/frontends/qt/GuiInputMethod.cpp 
b/src/frontends/qt/GuiInputMethod.cpp
index 47a4f63b88..98dfb2f0b1 100644
--- a/src/frontends/qt/GuiInputMethod.cpp
+++ b/src/frontends/qt/GuiInputMethod.cpp
@@ -30,6 +30,7 @@
 #include "TextMetrics.h"
 
 #include "support/debug.h"
+#include "support/lassert.h"
 #include "support/qstring_helpers.h"
 
 using namespace std;
@@ -64,6 +65,7 @@ struct GuiInputMethod::Private
        ParagraphMetrics * pm_ptr_ = nullptr;
 
        PreeditStyle style_;
+       std::vector<PreeditSegment> seg_turnout_;
 
        InputMethodState im_state_;
 
@@ -79,6 +81,7 @@ struct GuiInputMethod::Private
 
        bool real_boundary_    = false;
        bool virtual_boundary_ = false;
+       bool initial_tf_entry_ = false;;
 
        Point init_point_;
        Dimension cur_dim_;
@@ -226,7 +229,7 @@ void GuiInputMethod::processPreedit(QInputMethodEvent* ev)
        // initialize virtual caret to the anchor (real cursor) position
        d->init_point_ =
                initializeCaretCoords(d->cur_row_idx_,
-                                     d->real_boundary_ && 
!d->im_state_.edit_mode_);
+                                     d->real_boundary_ && 
!d->im_state_.composing_mode_);
 
        // Push preedit texts into row elements, which can shift the anchor
        // point of the preedit texts in a centered or right-flushed row.
@@ -272,36 +275,61 @@ void GuiInputMethod::setPreeditStyle(
 
        const QInputMethodEvent::Attribute * focus_style = nullptr;
 
-       // Since Qt6 and on MacOS, the initial entry seems to deliver 
information
-       // about the focused segment (undocumented). We formulate the code to
-       // utilize this fact keeping fail-safe against its failure.
-
 #if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
-       bool initial_tf_entry = true;
-#else
-       bool initial_tf_entry = false;
+       d->initial_tf_entry_ = true;
 #endif
-       // max segment position whose information we already have
-       pos_type max_start = -1;
 
+       // next char position to be set up the preedit style
+       pos_type next_seg_pos = 0;
+       //
+       d->seg_turnout_.clear();
+
+       LYXERR(Debug::GUI, "Start parsing attributes of the preedit");
        // obtain attributes of input method
        for (const QInputMethodEvent::Attribute & it : attr) {
 
                switch (it.type) {
                case QInputMethodEvent::TextFormat:
-                       // We adapt to the protocol on MacOS Qt6 that the first 
entry is the
-                       // style for the focused segment and the rest is the 
baseline styles
-                       // for all segments including a duplicate of the 
focused one.
-                       // Since Qt documentation says there should be at most 
one format
-                       // for every part of the preedit string, we ignore 
possibility of
-                       // other duplicate cases (at least they are not 
observed).
-
-                       if (initial_tf_entry) {
-                               // most likely the style for the focused segment
-                               focus_style = &it;
-                               initial_tf_entry = false;
-                       } else
-                               setTextFormat(it, focus_style, max_start);
+                       // Explanation on attributes of QInputMethodEvent:
+                       //     
https://doc.qt.io/archives/qt-5.15/qinputmethodevent.html
+                       //
+                       // CJK and European languages that use deadkeys use 
preedit strings.
+                       // Japanese language tends to use longer preedit 
strings so that
+                       // sorting of their text format attributes is generally 
required.
+                       // Even though the above documentation does not exclude 
random
+                       // arrival of any number of sections in the 
communication with the
+                       // input method, there is observed regularity in its 
arrival and the
+                       // number of sections used is at most three.
+                       //
+                       // Moreover, MacOS has a peculiarity that the text 
format attribute
+                       // for the focused section arrives twice, whose 
behavior is
+                       // undefined in the above documentation.
+                       //
+                       // Typical observed pattern on the completing stage is 
as follows.
+                       // Assuming that the second section is a focused one, 
the order of
+                       // the attributes is:
+                       //
+                       //     Linux/Windows: 2 1   3
+                       //     MacOS (Qt6)  : 2 1 2 3 (the first '2' conveys 
color info)
+                       //     MacOS (Qt5)  :   1 2 3
+                       //
+                       // (The number shows the order of sections.)
+                       //
+                       // That is, the first attribute is the focused section 
and other
+                       // sections follow in order.
+                       //
+                       // Preedit strings on the composing stage consists of 
only one
+                       // section on Qt5. However, MacOS on Qt6 appends a void 
section
+                       // with length zero whose position is at the end of the 
string
+                       // containing information of the same color as the 
focused section.
+                       // We will not use this information but need to prepare 
for this
+                       // pattern of information arrival.
+                       //
+                       // Whereas observed protocol shows a fixed pattern 
which we are
+                       // going to utilize, we need prepare for *any* type of 
information
+                       // arrival that satisfies the documented protocol.
+
+                       next_seg_pos = setTextFormat(it, next_seg_pos);
 
                        break;
 
@@ -348,6 +376,13 @@ void GuiInputMethod::setPreeditStyle(
                } // end switch
        } // 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()) {
+               LATTEST("Turnouts of preedit segments have not been all swept");
+       }
+
 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        // set background color for a focused segment
        if (!d->style_.segments_.empty()) {
@@ -362,16 +397,16 @@ void GuiInputMethod::setPreeditStyle(
        }
 #endif // QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
 
-       // edit mode: it has no focused segments
-       // edit mode in Qt version 6 or later gives focus_style->length == 0 and
+       // composing mode: it has no focused segments
+       // composing mode in Qt version 6 or later gives focus_style->length == 
0 and
        // focus_style->start > d->preedit_str_.length() on MacOS (undocumented)
-       d->im_state_.edit_mode_ =
+       d->im_state_.composing_mode_ =
                (d->style_.segments_.size() == 0 && focus_style != nullptr)
                || (focus_style != nullptr && focus_style->length == 0)
                || (focus_style == nullptr && d->style_.segments_.size() <= 1);
 
-       if (d->im_state_.edit_mode_) {
-               LYXERR(Debug::DEBUG, "preedit is in the edit mode");
+       if (d->im_state_.composing_mode_) {
+               LYXERR(Debug::DEBUG, "preedit is in the composing mode");
                QTextCharFormat char_format;
                int start = 0;
                size_type length = 0;
@@ -395,56 +430,122 @@ void GuiInputMethod::setPreeditStyle(
        }
 }
 
-void GuiInputMethod::setTextFormat(const QInputMethodEvent::Attribute & it,
-                                   const QInputMethodEvent::Attribute * 
focus_style,
-                                   pos_type & max_start)
+pos_type GuiInputMethod::setTextFormat(const QInputMethodEvent::Attribute & it,
+                                       pos_type next_seg_pos)
 {
        // get LyX's color setting
        QTextCharFormat char_format = it.value.value<QTextCharFormat>();
 
        LYXERR(Debug::GUI,
               "QInputMethodEvent::TextFormat start: " << it.start <<
-              " length: " << it.length <<
+              " end: " << it.start + it.length - 1 <<
               " underline? " << char_format.font().underline() <<
               " UnderlineStyle: " << char_format.underlineStyle() <<
               " fg: " << char_format.foreground().color().name() <<
               " bg: " << char_format.background().color().name());
 
-       // take union of the current style and the focus style
-       // the last condition clears background color in the edit mode
-       // this is simply from a (subjective) aethetic consideration
-       if (focus_style != nullptr &&
-               (it.start == focus_style->start || focus_style->length == 0) &&
-               it.length < (int)d->preedit_str_.length() /* completing mode*/)
-               char_format.merge(focus_style->value.value<QTextCharFormat>());
-
+       //
+       // Fit and adjust arrived text formats
+       //
        conformToSurroundingFont(char_format);
-
+       // QLocale is used for wrapping words
+       char_format.setProperty(QMetaType::QLocale, d->style_.lang_);
 #if (! defined(Q_OS_MACOS)) || QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        char_format.setFontUnderline(true);
 #endif
 
-       // QLocale is used for wrapping words
-       char_format.setProperty(QMetaType::QLocale, d->style_.lang_);
 
-       // Do we already have some information about the incoming segment?
-       // On Linux, same information arrives repeatedly from IM (a bug? 
2025/1/10)
-       if (it.start <= max_start) {
-               for (auto & seg : d->style_.segments_) {
-                       if (it.start == seg.start_ &&
-                               (it.length == (int)seg.length_ || it.length == 
0))
-                               // merge old and new information on style
-                               seg.char_format_.merge(char_format);
+       // The void "cursor segment" in the composing mode comes with it.start 
> 0 and
+       // 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_
+                       pos_type updated_pos =
+                               pickNextSegFromTurnout(next_seg_pos, 
&char_format);
+                       if (updated_pos == next_seg_pos) {
+                               // no matching segment in the turnout
+                               LYXERR(Debug::GUI, "Pushing to preedit 
register: (" << it.start
+                                      << ", " << it.start + it.length - 1 << 
") color: "
+                                      << 
char_format.background().color().name());
+                               next_seg_pos = registerSegment(it.start, 
(size_type)it.length,
+                                                              char_format);
+                       } else
+                               next_seg_pos = updated_pos;
+               } else {
+                       // push the constructed char format together with start 
and length
+                       // to the list
+                       LYXERR(Debug::GUI, "Pushing to preedit register: (" << 
it.start
+                              << ", " << it.start + it.length - 1 << ") color: 
"
+                              << char_format.background().color().name());
+                       next_seg_pos =
+                               registerSegment(it.start, (size_type)it.length, 
char_format);
                }
-       } else {
-               // push the constructed char format together with start and 
length
-               // to the list
-               PreeditSegment seg = {it.start, (size_type)it.length, 
char_format};
-               d->style_.segments_.push_back(seg);
+               next_seg_pos = pickNextSegFromTurnout(next_seg_pos);
+       } else if ((it.start > next_seg_pos || d->initial_tf_entry_) && 
it.length > 0) {
+               LYXERR(Debug::GUI, "Pushing to preedit turnout:  (" << it.start 
<< ", "
+                               << it.start + it.length - 1 << ")");
+               PreeditSegment turnout = {it.start, (size_type)it.length, 
char_format};
+               d->seg_turnout_.push_back(turnout);
+               d->initial_tf_entry_ = false;
        }
+       return next_seg_pos;
+}
+
+
+pos_type GuiInputMethod::pickNextSegFromTurnout(pos_type next_seg_pos,
+                                                QTextCharFormat * cf) {
+       std::vector<PreeditSegment>::iterator to_erase;
+       bool is_matched = false;
+
+       // 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) {
+                       if (cf != nullptr) {
+                               (*past_attr).char_format_.merge(*cf);
+                       }
+                       PreeditSegment seg = {(*past_attr).start_,
+                                                                 
(size_type)(*past_attr).length_,
+                                                                 
(*past_attr).char_format_};
+                       LYXERR(Debug::GUI,
+                              "Pushing to preedit register: (" << 
(*past_attr).start_
+                               << ", " << (*past_attr).start_ + 
(*past_attr).length_ - 1
+                               << ") color: "
+                               << 
(*past_attr).char_format_.background().color().name());
+                       d->style_.segments_.push_back(seg);
+                       next_seg_pos += (*past_attr).length_;
+                       if (d->seg_turnout_.size() > 1)
+                               to_erase = 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::GUI, "Preedit turnout clearing:    ("
+                               << d->seg_turnout_.back().start_ << ", "
+                               << d->seg_turnout_.back().start_
+                                      + d->seg_turnout_.back().length_ - 1 << 
")");
+                       d->seg_turnout_.pop_back();
+               } else if (d->seg_turnout_.size() > 1) {
+                       LYXERR(Debug::GUI, "Preedit turnout clearing: ("
+                               << (*to_erase).start_ << ", "
+                               << (*to_erase).start_ + (*to_erase).length_ - 1 
<< ")");
+                       d->seg_turnout_.erase(to_erase);
+               }
+       }
+
+       return next_seg_pos;
+}
+
 
-       if (it.start > max_start)
-               max_start = it.start;
+pos_type GuiInputMethod::registerSegment(pos_type start, size_type length,
+                                         QTextCharFormat char_format) {
+       PreeditSegment seg = {start, length, char_format};
+       d->style_.segments_.push_back(seg);
+       return start + length;
 }
 
 
@@ -523,7 +624,7 @@ std::array<int,2> GuiInputMethod::setCaretOffset(pos_type 
caret_pos){
        QString str_before_caret = "";
 
 #if defined(Q_OS_MACOS) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
-       if (d->im_state_.edit_mode_)
+       if (d->im_state_.composing_mode_)
                str_before_caret =
                    toqstr(d->preedit_str_.substr(0, caret_pos - 
*d->cur_pos_ptr_));
        else
@@ -580,13 +681,13 @@ std::array<int,2> GuiInputMethod::setCaretOffset(pos_type 
caret_pos){
                        inset_offset = 0;
                }
 #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
-               if (d->real_boundary_ && !d->im_state_.edit_mode_)
+               if (d->real_boundary_ && !d->im_state_.composing_mode_)
                        caret_offset[0] = qfm.horizontalAdvance(lastline_str);
                else
                        caret_offset[0] = - d->init_point_.x + left_margin
                                + inset_offset + 
qfm.horizontalAdvance(lastline_str);
 #else
-               if (d->real_boundary_ && !d->im_state_.edit_mode_)
+               if (d->real_boundary_ && !d->im_state_.composing_mode_)
                        caret_offset[0] = qfm.width(lastline_str);
                else
                        caret_offset[0] = - d->init_point_.x + left_margin
@@ -608,9 +709,9 @@ std::array<int,2> GuiInputMethod::setCaretOffset(pos_type 
caret_pos){
        // vertical offset only applicable to main text
        caret_offset[1] = 0;
        for (pos_type i = d->cur_row_idx_ -
-            (d->real_boundary_ && d->im_state_.edit_mode_);
+            (d->real_boundary_ && d->im_state_.composing_mode_);
             i < caret_row.index -
-            (d->real_boundary_ && !d->im_state_.edit_mode_); ++i)
+            (d->real_boundary_ && !d->im_state_.composing_mode_); ++i)
                caret_offset[1] += d->rows_[i].descent() + 
d->rows_[i+1].ascent();
 
        return caret_offset;
@@ -712,14 +813,14 @@ void GuiInputMethod::processQuery(Qt::InputMethodQuery 
query)
        // the context menu position when the menu key is pressed.
        case Qt::ImCursorRectangle: {
                QRectF * rect;
-               if (d->im_state_.edit_mode_) {
+               if (d->im_state_.composing_mode_) {
                        // in the editing mode, cursor_rect_ follows the 
position of the
                        // virtual caret, but the drop down of predicted 
candidates wants
                        // the starting point of the preedit, so respond with 
anchor_rect_
                        // that points the starting point during the editing 
mode
                        rect = &d->im_state_.anchor_rect_;
                        LYXERR(Debug::DEBUG,
-                              "     (Edit mode: use anchor_rect_ for 
ImCursorRectangle)");
+                              "     (Composing mode: use anchor_rect_ for 
ImCursorRectangle)");
                } else
                        rect = &d->im_state_.cursor_rect_;
 
@@ -1040,7 +1141,7 @@ GuiInputMethod::PreeditRow GuiInputMethod::getCaretInfo(
        // when real_boundary is true, cursor position is at the beginning of 
the
        // new line, while the caret on screen stays at the end of one line 
above
        // below is the starting point to calculate caret_row.pos
-       caret_row.pos = (real_boundary && !d->im_state_.edit_mode_) ?
+       caret_row.pos = (real_boundary && !d->im_state_.composing_mode_) ?
                    *d->cur_pos_ptr_ : second_row_pos;
 
        // if the preedit caret is on the second row or later, count the second 
row
diff --git a/src/frontends/qt/GuiInputMethod.h 
b/src/frontends/qt/GuiInputMethod.h
index 684088a37d..91b9011ef5 100644
--- a/src/frontends/qt/GuiInputMethod.h
+++ b/src/frontends/qt/GuiInputMethod.h
@@ -65,8 +65,8 @@ public:
 
        struct InputMethodState {
                bool      enabled_ = true;
-               bool      preediting_ = false; // either in edit or completing 
mode
-               bool      edit_mode_ = true;  // i.e. not completing mode
+               bool      preediting_ = false; // either in composing or 
completing mode
+               bool      composing_mode_ = true;  // i.e. not completing mode
                QRectF    cursor_rect_;
                QRectF    anchor_rect_;
                docstring surrounding_text_;
@@ -147,10 +147,9 @@ private:
        /// Aquire and set character style of each preedit segment from
        /// attributes of the incoming input method event
        void setPreeditStyle(const QList<QInputMethodEvent::Attribute> & attr);
-       /// Sets TextFormat
-       void setTextFormat(const QInputMethodEvent::Attribute & it,
-                          const QInputMethodEvent::Attribute * focus_style,
-                          pos_type & max_start);
+       /// Sets TextFormat. Returns next pos of finished text
+       pos_type setTextFormat(const QInputMethodEvent::Attribute & it,
+                              pos_type next_seg_pos);
        /// Set QTextCharFormat to fit the font used in the surrounding text
        void conformToSurroundingFont(QTextCharFormat & char_format);
        /// Returns index of the focused segment
@@ -159,7 +158,12 @@ private:
        int shiftFromCaretToSegmentHead();
        ///
        PreeditRow getCaretInfo(const bool real_boundary, const bool 
virtual_boundary);
-
+       /// 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);
+       /// 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
        docstring inputMethodQueryFlagsAsString(unsigned long int query) const;
 
diff --git a/src/frontends/qt/GuiPainter.cpp b/src/frontends/qt/GuiPainter.cpp
index a775d33886..0e705dcaf3 100644
--- a/src/frontends/qt/GuiPainter.cpp
+++ b/src/frontends/qt/GuiPainter.cpp
@@ -365,6 +365,7 @@ void GuiPainter::text(int x, int y, docstring const & s,
        else setLayoutDirection(Qt::LeftToRight);
        GuiInputMethod const * gim = dynamic_cast<GuiInputMethod const *>(im);
        LATTEST(gim);
+       const QPen orig_pen = pen();
        setPen(gim->charFormat(char_format_index).foreground().color());
        setBackgroundMode(Qt::OpaqueMode);
        setBackground(gim->charFormat(char_format_index).background());
@@ -378,6 +379,7 @@ void GuiPainter::text(int x, int y, docstring const & s,
 
        drawText(x, y, str);
        setBackgroundMode(Qt::TransparentMode);
+       setPen(orig_pen);
 }
 
 void GuiPainter::text(int x, int y, docstring const & str, Font const & f,
-- 
lyx-cvs mailing list
[email protected]
https://lists.lyx.org/mailman/listinfo/lyx-cvs

Reply via email to