]> git.lyx.org Git - lyx.git/blobdiff - src/frontends/qt/GuiWorkArea.cpp
Fix up 'Reduce metrics updates from 4 to 1 when loading file'
[lyx.git] / src / frontends / qt / GuiWorkArea.cpp
index f6e3c723dfdee2e78597be93177bfab111aabf1f..5935baaf77686ffd93ef052787631de913a9aa64 100644 (file)
@@ -54,6 +54,7 @@
 #include <QContextMenuEvent>
 #include <QDrag>
 #include <QHelpEvent>
+#include <QInputMethod>
 #ifdef Q_OS_MAC
 #include <QProxyStyle>
 #endif
@@ -178,7 +179,11 @@ GuiWorkArea::GuiWorkArea(Buffer & buffer, GuiView & gv)
 
 double GuiWorkArea::pixelRatio() const
 {
-       return qt_scale_factor * devicePixelRatio();
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
+       return devicePixelRatioF();
+#else
+       return devicePixelRatio();
+#endif
 }
 
 
@@ -194,9 +199,6 @@ void GuiWorkArea::init()
                });
 
        d->resetScreen();
-       // A mouse event will happen before the first paint event,
-       // so make sure that the buffer view has an up to date metrics.
-       d->buffer_view_->resize(viewport()->width(), viewport()->height());
 
        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        setAcceptDrops(true);
@@ -218,6 +220,14 @@ void GuiWorkArea::init()
        // Enables input methods for asian languages.
        // Must be set when creating custom text editing widgets.
        setAttribute(Qt::WA_InputMethodEnabled, true);
+
+       // Initialize d->im_cursor_rect_
+       Point point;
+       Dimension dim;
+       d->buffer_view_->caretPosAndDim(point, dim);
+       int cur_x = point.x_ - dim.width();
+       int cur_y = point.y_ + dim.height();
+       d->im_cursor_rect_ = QRectF(cur_x, (cur_y - dim.height()) , 1, dim.height() );
 }
 
 
@@ -276,6 +286,9 @@ void GuiWorkArea::close()
 void GuiWorkArea::setFullScreen(bool full_screen)
 {
        d->buffer_view_->setFullScreen(full_screen);
+
+       queryInputItemTransform();
+
        if (full_screen && lyxrc.full_screen_scrollbar)
                setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        else
@@ -338,7 +351,7 @@ void GuiWorkArea::toggleCaret()
 
 void GuiWorkArea::scheduleRedraw(bool update_metrics)
 {
-       if (!isVisible())
+       if (!isVisible() || view().busy())
                // No need to redraw in this case.
                return;
 
@@ -442,33 +455,33 @@ void GuiWorkArea::Private::dispatch(FuncRequest const & cmd)
 }
 
 
-void GuiWorkArea::Private::resizeBufferView()
+void GuiWorkArea::resizeBufferView()
 {
        // WARNING: Please don't put any code that will trigger a repaint here!
        // We are already inside a paint event.
-       p->stopBlinkingCaret();
+       stopBlinkingCaret();
        // Warn our container (GuiView).
-       p->busy(true);
+       busy(true);
 
-       bool const caret_in_view = buffer_view_->caretInView();
-       buffer_view_->resize(p->viewport()->width(), p->viewport()->height());
+       bool const caret_in_view = d->buffer_view_->caretInView();
+       d->buffer_view_->resize(viewport()->width(), viewport()->height());
        if (caret_in_view)
-               buffer_view_->showCursor();
-       resetCaret();
+               d->buffer_view_->showCursor();
+       d->resetCaret();
 
        // Update scrollbars which might have changed due different
        // BufferView dimension. This is especially important when the
        // BufferView goes from zero-size to the real-size for the first time,
        // as the scrollbar parameters are then set for the first time.
-       updateScrollbar();
+       d->updateScrollbar();
 
-       need_resize_ = false;
-       p->busy(false);
+       d->need_resize_ = false;
+       busy(false);
        // Eventually, restart the caret after the resize event.
        // We might be resizing even if the focus is on another widget so we only
        // restart the caret if we have the focus.
-       if (p->hasFocus())
-               QTimer::singleShot(50, p, SLOT(startBlinkingCaret()));
+       if (hasFocus())
+               QTimer::singleShot(50, this, SLOT(startBlinkingCaret()));
 }
 
 
@@ -652,6 +665,10 @@ void GuiWorkArea::contextMenuEvent(QContextMenuEvent * e)
                                        ++pos.rx();
                        }
                }
+               if (e->reason() == QContextMenuEvent::Keyboard)
+                       // Subtract the top margin, see #12811
+                       pos.setY(pos.y() - d->buffer_view_->topMargin());
+
                name = d->buffer_view_->contextMenu(pos.x(), pos.y());
        }
 
@@ -676,12 +693,22 @@ void GuiWorkArea::contextMenuEvent(QContextMenuEvent * e)
 
 void GuiWorkArea::focusInEvent(QFocusEvent * e)
 {
-       LYXERR(Debug::DEBUG, "GuiWorkArea::focusInEvent(): " << this << endl);
+       LYXERR(Debug::DEBUG, "GuiWorkArea::focusInEvent(): " << this << " reason() = " << e->reason() << endl);
        if (d->lyx_view_->currentWorkArea() != this) {
                d->lyx_view_->setCurrentWorkArea(this);
                d->lyx_view_->currentWorkArea()->bufferView().buffer().updateBuffer();
        }
 
+       // needs to reset IM item coordinates when focus in from dialogs or other apps
+       if ((e->reason() == Qt::PopupFocusReason || e->reason() == Qt::ActiveWindowFocusReason) &&
+               !(this->inDialogMode())) {
+               // Switched from most of dialogs or other apps, and not on a dialog (e.g. findreplaceadv)
+               d->item_trans_needs_reset_ = true;
+       } else {
+               // Switched from advanced search dialog or else (e.g. mouse event)
+               d->item_trans_needs_reset_ = false;
+       }
+
        startBlinkingCaret();
        QAbstractScrollArea::focusInEvent(e);
 }
@@ -960,7 +987,7 @@ void GuiWorkArea::generateSyntheticMouseEvent()
        // Find the row at which we set the cursor.
        RowList::const_iterator rit = pm.rows().begin();
        RowList::const_iterator rlast = pm.rows().end();
-       int yy = pm.position() - pm.ascent();
+       int yy = pm.top();
        for (--rlast; rit != rlast; ++rit) {
                int h = rit->height();
                if ((up && yy + h > 0)
@@ -1111,20 +1138,87 @@ void GuiWorkArea::resizeEvent(QResizeEvent * ev)
 }
 
 
+void GuiWorkArea::queryInputItemTransform()
+{
+       LYXERR(
+                  Debug::DEBUG,
+                  "item_trans_ is aquired: dx() = " << d->item_trans_.dx() <<
+                  " -> " << d->im_->inputItemTransform().dx() <<
+                  ", dy() = " << d->item_trans_.dy() <<
+                  " -> " << d->im_->inputItemTransform().dy()
+                  );
+
+       d->item_trans_ = d->im_->inputItemTransform();
+}
+
+
+void GuiWorkArea::Private::resetInputItemTransform()
+{
+       if (item_trans_needs_reset_) {
+               LYXERR(
+                          Debug::DEBUG,
+                          "(" << this <<
+                          ") item_trans_ is reset: dx() = " << im_->inputItemTransform().dx() <<
+                          " -> " << item_trans_.dx() <<
+                          ", dy() = " << im_->inputItemTransform().dy() <<
+                          " -> " << item_trans_.dy()
+                          );
+               im_->setInputItemTransform(item_trans_);
+               item_trans_needs_reset_ = false;
+       }
+}
+
+
+//#define DEBUG_PREEDIT
+
 void GuiWorkArea::Private::paintPreeditText(GuiPainter & pain)
 {
-       if (preedit_string_.empty())
+#ifdef DEBUG_PREEDIT
+       // check the language that current input method uses
+       QLocale::Language lang = im_->locale().language();
+       if (lang != im_lang_) {
+               LYXERR0("QLocale = " << QLocale::languageToString(lang));
+               im_lang_ = lang;
+       }
+#endif
+               
+       // Chinese IM may want cursor position even when preedit string is empty
+       // such a case is handled below
+       if (preedit_string_.empty() && im_->locale().language() != QLocale::Chinese)
                return;
 
-       // FIXME: shall we use real_current_font here? (see #10478)
-       FontInfo const font = buffer_view_->cursor().getFont().fontInfo();
-       FontMetrics const & fm = theFontMetrics(font);
+       // lower margin of the preedit area to separate the candidate window
+       // report to IM the height of preedit rectangle larger than the actual by
+       // preedit_lower_margin so that the conversion suggestion window does not
+       // hide the underline of the preedit text
+       int preedit_lower_margin = 1;
+
        Point point;
        Dimension dim;
        buffer_view_->caretPosAndDim(point, dim);
-       int cur_x = point.x_;
+       int cur_x = point.x_ - dim.width();
        int cur_y = point.y_ + dim.height();
 
+       if (preedit_string_.empty()) {
+               // Chinese input methods may exit here just obtaining im_cursor_rect
+               im_cursor_rect_ =
+                       QRectF(cur_x, cur_y - dim.height(), 1, dim.height() + preedit_lower_margin);
+               im_->update(Qt::ImCursorRectangle);
+               return;
+       }
+
+       // reset item transformation since it can go wrong after the item gets
+       // lost and regains focus or after a new tab (dis)appears etc.
+       resetInputItemTransform();
+
+       // FIXME: shall we use real_current_font here? (see #10478)
+       FontInfo const font = buffer_view_->cursor().getFont().fontInfo();
+       FontMetrics const & fm = theFontMetrics(font);
+
+       // force fulldraw to remove previous paint remaining on screen
+    // FIXME: This is costly to do
+       buffer_view_->processUpdateFlags(Update::ForceDraw);
+
        // get attributes of input method cursor.
        // cursor_pos : cursor position in preedit string.
        size_t cursor_pos = 0;
@@ -1164,7 +1258,8 @@ void GuiWorkArea::Private::paintPreeditText(GuiPainter & pain)
                rLength = 0;
        }
 
-       int const right_margin = buffer_view_->rightMargin();
+       int const text_width = p->viewport()->width() - buffer_view_->rightMargin()
+                       - buffer_view_->leftMargin();
        Painter::preedit_style ps;
        // Most often there would be only one line:
        preedit_lines_ = 1;
@@ -1174,8 +1269,8 @@ void GuiWorkArea::Private::paintPreeditText(GuiPainter & pain)
                ps = Painter::preedit_default;
 
                // if we reached the right extremity of the screen, go to next line.
-               if (cur_x + fm.width(typed_char) > p->viewport()->width() - right_margin) {
-                       cur_x = right_margin;
+               if (cur_x + fm.width(typed_char) > p->viewport()->width() - buffer_view_->rightMargin()) {
+                       cur_x = buffer_view_->leftMargin();
                        cur_y += dim.height() + 1;
                        ++preedit_lines_;
                }
@@ -1186,8 +1281,9 @@ void GuiWorkArea::Private::paintPreeditText(GuiPainter & pain)
                // FIXME: should be put out of the loop.
                if (pos >= rStart
                        && pos < rStart + rLength
-                       && !(cursor_pos < rLength && rLength == preedit_length))
+                       && !(cursor_pos < rLength && rLength == preedit_length)) {
                        ps = Painter::preedit_selecting;
+               }
 
                if (pos == cursor_pos
                        && (cursor_pos < rLength && rLength == preedit_length))
@@ -1196,15 +1292,58 @@ void GuiWorkArea::Private::paintPreeditText(GuiPainter & pain)
                // draw one character and update cur_x.
                cur_x += pain.preeditText(cur_x, cur_y, typed_char, font, ps);
        }
+
+       // the selection candidate window follows the position of Qt::ImCursorRectangle
+       // so we set it here in preparation for InputMethodQuery.
+       if(rLength > 0) {
+               // candidate selection mode
+               div_t mod_cur, mod_anc;
+               // width of preedit string in pixels
+               int preedit_width = fm.width(preedit_string_);
+               // calculate for modular so that its remainder and quotient become horizontal
+               // and vertical adjustment factor respectively
+               // FIXME: divider should be (text_width - font width of the end-of-line character)
+               //        however, since it's very rare that preedit becomes so long that it goes over
+               //        more than two lines, current error of position is more or less negligible
+               mod_cur = div(text_width + cur_x - (preedit_width - fm.width(preedit_string_.substr(0, rStart))),
+                               text_width);
+               mod_anc = div(text_width + cur_x - (preedit_width - fm.width(preedit_string_.substr(0, rStart + rLength))),
+                               text_width);
+               // Obtain cursor and anchor rectangles to bind starting and ending points of selection.
+               // Since the remainder of div moves from positive to negative as the line becomes longer
+               // while its quotient repeats zero twice, we need to take it into account by conditioning.
+               if (mod_cur.rem >= 0)
+                       im_cursor_rect_ = QRectF(mod_cur.rem,
+                                       (cur_y - dim.height()) + (dim.height() + 1) * (mod_cur.quot - 1), 1,
+                                       dim.height() + preedit_lower_margin);
+               else
+                       im_cursor_rect_ = QRectF(text_width + mod_cur.rem,
+                                       (cur_y - dim.height()) + (dim.height() + 1) * (mod_cur.quot - 2), 1,
+                                       dim.height() + preedit_lower_margin);
+               if (mod_anc.rem >= 0)
+                       im_anchor_rect_ = QRectF(mod_anc.rem,
+                                       point.y_ + (dim.height() + 1) * (mod_anc.quot - 1), 1,
+                                       dim.height() + preedit_lower_margin );
+               else
+                       im_anchor_rect_ = QRectF(text_width + mod_anc.rem,
+                                       point.y_ + (dim.height() + 1) * (mod_anc.quot - 2), 1,
+                                       dim.height() + preedit_lower_margin );
+       } else {
+               im_cursor_rect_ =       QRectF(point.x_, point.y_, 1, dim.height() + preedit_lower_margin);
+               im_anchor_rect_ = im_cursor_rect_;
+       }
+       // Urge platform input method to make inputMethodQuery to check the values
+       // set above
+       im_->update(Qt::ImQueryInput);
 }
 
 
 void GuiWorkArea::Private::resetScreen()
 {
        if (use_backingstore_) {
-               int const pr = p->pixelRatio();
-               screen_ = QImage(pr * p->viewport()->width(),
-                                pr * p->viewport()->height(),
+               double const pr = p->pixelRatio();
+               screen_ = QImage(int(pr * p->viewport()->width()),
+                                int(pr * p->viewport()->height()),
                                 QImage::Format_ARGB32_Premultiplied);
                screen_.setDevicePixelRatio(pr);
        }
@@ -1254,7 +1393,7 @@ void GuiWorkArea::paintEvent(QPaintEvent * ev)
 
        if (d->need_resize_ || pixelRatio() != d->last_pixel_ratio_) {
                d->resetScreen();
-               d->resizeBufferView();
+               resizeBufferView();
        }
 
        d->last_pixel_ratio_ = pixelRatio();
@@ -1348,15 +1487,20 @@ void GuiWorkArea::inputMethodEvent(QInputMethodEvent * e)
 
 QVariant GuiWorkArea::inputMethodQuery(Qt::InputMethodQuery query) const
 {
+       LYXERR(Debug::INFO, "incoming InputMethodQuery Value: 0x" << std::hex << query);
        switch (query) {
-               // this is the CJK-specific composition window position and
-               // the context menu position when the menu key is pressed.
+       // this is the CJK-specific composition window position and
+       // the context menu position when the menu key is pressed.
        case Qt::ImCursorRectangle: {
-               CaretGeometry const & cg = bufferView().caretGeometry();
-               return QRect(cg.left - 10 * (d->preedit_lines_ != 1),
-                            cg.top + cg.height() * d->preedit_lines_,
-                            cg.width(), cg.height());
+               return QVariant(d->im_cursor_rect_);
+               break;
+       }
+#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
+       case Qt::ImAnchorRectangle: {
+               return QVariant(d->im_anchor_rect_);
+               break;
        }
+#endif
        default:
                return QWidget::inputMethodQuery(query);
        }
@@ -1499,6 +1643,9 @@ TabWorkArea::TabWorkArea(QWidget * parent)
 
        QObject::connect(this, SIGNAL(currentChanged(int)),
                this, SLOT(on_currentTabChanged(int)));
+       // Fix for #11835
+       QObject::connect(this, SIGNAL(tabBarClicked(int)),
+               this, SLOT(on_currentTabChanged(int)));
 
        closeBufferButton = new QToolButton(this);
        closeBufferButton->setPalette(pal);
@@ -1711,7 +1858,7 @@ bool TabWorkArea::setCurrentWorkArea(GuiWorkArea * work_area)
        else
                // Switch to the work area.
                setCurrentIndex(index);
-       work_area->setFocus();
+       work_area->setFocus(Qt::OtherFocusReason);
 
        return true;
 }
@@ -1719,6 +1866,7 @@ bool TabWorkArea::setCurrentWorkArea(GuiWorkArea * work_area)
 
 GuiWorkArea * TabWorkArea::addWorkArea(Buffer & buffer, GuiView & view)
 {
+       view.setBusy(true);
        GuiWorkArea * wa = new GuiWorkArea(buffer, view);
        GuiWorkAreaContainer * wac = new GuiWorkAreaContainer(wa);
        wa->setUpdatesEnabled(false);
@@ -1737,6 +1885,13 @@ GuiWorkArea * TabWorkArea::addWorkArea(Buffer & buffer, GuiView & view)
 
        updateTabTexts();
 
+       // obtain new input item coordinates in the new and old work areas
+       wa->queryInputItemTransform();
+       if (currentWorkArea())
+               currentWorkArea()->queryInputItemTransform();
+
+       view.setBusy(false);
+
        return wa;
 }
 
@@ -1761,6 +1916,7 @@ bool TabWorkArea::removeWorkArea(GuiWorkArea * work_area)
                else
                        // Show tabbar only if there's more than one tab.
                        showBar(count() > 1);
+               currentWorkArea()->queryInputItemTransform();
        } else
                lastWorkAreaRemoved();
 
@@ -2076,13 +2232,13 @@ void TabWorkArea::showContextMenu(const QPoint & pos)
        // show tab popup
        QMenu popup;
        popup.addAction(QIcon(getPixmap("images/", "hidetab", "svgz,png")),
-               qt_("Hide tab"), this, SLOT(hideCurrentTab()));
+               qt_("&Hide Tab"), this, SLOT(hideCurrentTab()));
 
        // we want to show the 'close' option only if this is not a child buffer.
        Buffer const & buf = wa->bufferView().buffer();
        if (!buf.parent())
                popup.addAction(QIcon(getPixmap("images/", "closetab", "svgz,png")),
-                       qt_("Close tab"), this, SLOT(closeCurrentBuffer()));
+                       qt_("&Close Tab"), this, SLOT(closeCurrentBuffer()));
        popup.exec(tabBar()->mapToGlobal(pos));
 
        clicked_tab_ = -1;