[LyX/2.4.1-devel] Fix wrong position of conversion windows of the input method #11723, #13054

Koji Yokota yokota at lyx.org
Fri Apr 12 13:43:38 UTC 2024


commit 76dbb9dc40a8568f15a6ba317ff05f016e2c5bbe
Author: Koji Yokota <yokota at lyx.org>
Date:   Fri Mar 29 22:09:36 2024 +0900

    Fix wrong position of conversion windows of the input method #11723, #13054
---
 src/frontends/qt/Dialog.cpp            |  17 ++---
 src/frontends/qt/Dialog.h              |  22 ++++++-
 src/frontends/qt/FindAndReplace.cpp    |   4 +-
 src/frontends/qt/GuiView.cpp           |  26 ++++++--
 src/frontends/qt/GuiView.h             |   1 +
 src/frontends/qt/GuiWorkArea.cpp       | 116 ++++++++++++++++++++++++++++-----
 src/frontends/qt/GuiWorkArea.h         |   2 +
 src/frontends/qt/GuiWorkArea_Private.h |   9 ++-
 8 files changed, 162 insertions(+), 35 deletions(-)

diff --git a/src/frontends/qt/Dialog.cpp b/src/frontends/qt/Dialog.cpp
index 783f5eb22c..35c40d041b 100644
--- a/src/frontends/qt/Dialog.cpp
+++ b/src/frontends/qt/Dialog.cpp
@@ -120,7 +120,7 @@ Buffer const & Dialog::documentBuffer() const
 }
 
 
-void Dialog::showData(string const & data)
+void Dialog::showData(string const & data, Qt::FocusReason reason)
 {
 	if (isBufferDependent() && !isBufferAvailable())
 		return;
@@ -131,7 +131,7 @@ void Dialog::showData(string const & data)
 		return;
 	}
 
-	showView();
+	showView(reason);
 }
 
 
@@ -169,25 +169,26 @@ void Dialog::prepareView()
 }
 
 
-void Dialog::showView()
+void Dialog::showView(Qt::FocusReason reason)
 {
 	prepareView();
 
 	QWidget * w = asQWidget();
-	if (!w->isVisible())
-		w->show();
+    if (!w->isVisible()) {
+        w->setFocus(reason);
+        w->show();
+    }
 	w->raise();
 	w->activateWindow();
 	if (wantInitialFocus())
-		w->setFocus();
+		w->setFocus(reason);
 	else {
 		lyxview_.raise();
 		lyxview_.activateWindow();
-		lyxview_.setFocus();
+		lyxview_.setFocus(reason);
 	}
 }
 
-
 void Dialog::hideView()
 {
 	QWidget * w = asQWidget();
diff --git a/src/frontends/qt/Dialog.h b/src/frontends/qt/Dialog.h
index 6968aea803..928513c3fe 100644
--- a/src/frontends/qt/Dialog.h
+++ b/src/frontends/qt/Dialog.h
@@ -88,7 +88,18 @@ public:
 	//@{
 	/// \param data is a string encoding of the data to be displayed.
 	/// It is passed to the Controller to be translated into a usable form.
-	virtual void showData(std::string const & data);
+    virtual void showData(std::string const & data)
+    {
+        return showData(data, Qt::PopupFocusReason);
+    }
+
+    /// \param data is a string encoding of the data to be displayed.
+    /// It is passed to the Controller to be translated into a usable form.
+    /// \param reason provides the focus reason of the dialog.
+    /// E.g. dialog "findreplaceadv" requires special treatment after
+    /// obtaining focus in terms of the input method item transformation,
+    /// so should be marked as reason = Qt::OtherFocusReason.
+    virtual void showData(std::string const & data, Qt::FocusReason reason);
 	//@}
 
 	/// \return inset at current cursor location.
@@ -121,7 +132,14 @@ public:
 	void hideView();
 
 	/// Prepare dialog and display it.
-	void showView();
+    void showView() { return showView(Qt::PopupFocusReason); }
+
+    /// Prepare dialog and display it.
+    /// \param reason provides the focus reason of the dialog.
+    /// E.g. dialog "findreplaceadv" requires special treatment after
+    /// obtaining focus in terms of the input method item transformation,
+    /// so should be marked as reason = Qt::OtherFocusReason.
+    void showView(Qt::FocusReason reason);
 
 	/// Prepare dialog before view.
 	void prepareView();
diff --git a/src/frontends/qt/FindAndReplace.cpp b/src/frontends/qt/FindAndReplace.cpp
index b53c520974..d61636af62 100644
--- a/src/frontends/qt/FindAndReplace.cpp
+++ b/src/frontends/qt/FindAndReplace.cpp
@@ -122,7 +122,7 @@ bool FindAndReplaceWidget::eventFilter(QObject * obj, QEvent * event)
 			FuncRequest func = theTopLevelKeymap().getBinding(seq);
 			if (!getStatus(func).enabled()) {
 				LYXERR(Debug::FINDVERBOSE, "Focusing replace WA");
-				replace_work_area_->setFocus();
+				replace_work_area_->setFocus(Qt::TabFocusReason);
 				LYXERR(Debug::FINDVERBOSE, "Selecting entire replace buffer");
 				dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
 				dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
@@ -138,7 +138,7 @@ bool FindAndReplaceWidget::eventFilter(QObject * obj, QEvent * event)
 			FuncRequest func = theTopLevelKeymap().getBinding(seq);
 			if (!getStatus(func).enabled()) {
 				LYXERR(Debug::FINDVERBOSE, "Focusing find WA");
-				find_work_area_->setFocus();
+				find_work_area_->setFocus(Qt::BacktabFocusReason);
 				LYXERR(Debug::FINDVERBOSE, "Selecting entire find buffer");
 				dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
 				dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
diff --git a/src/frontends/qt/GuiView.cpp b/src/frontends/qt/GuiView.cpp
index 6f15d73626..c44fbb3ed1 100644
--- a/src/frontends/qt/GuiView.cpp
+++ b/src/frontends/qt/GuiView.cpp
@@ -1217,6 +1217,13 @@ void GuiView::setFocus()
 }
 
 
+void GuiView::setFocus(Qt::FocusReason reason)
+{
+    LYXERR(Debug::DEBUG, "GuiView::setFocus()" << this << " reason = " << reason);
+    QMainWindow::setFocus(reason);
+}
+
+
 bool GuiView::hasFocus() const
 {
 	if (currentWorkArea())
@@ -1234,11 +1241,11 @@ void GuiView::focusInEvent(QFocusEvent * e)
 	// Make sure guiApp points to the correct view.
 	guiApp->setCurrentView(this);
 	if (currentWorkArea())
-		currentWorkArea()->setFocus();
+		currentWorkArea()->setFocus(e->reason());
 	else if (currentMainWorkArea())
-		currentMainWorkArea()->setFocus();
+		currentMainWorkArea()->setFocus(e->reason());
 	else
-		d.bg_widget_->setFocus();
+		d.bg_widget_->setFocus(e->reason());
 }
 
 
@@ -1571,6 +1578,9 @@ void GuiView::on_currentWorkAreaChanged(GuiWorkArea * wa)
 	connectBufferView(wa->bufferView());
 	connectBuffer(wa->bufferView().buffer());
 	d.current_work_area_ = wa;
+    // The below specifies that the input method item transformation will
+    // not reset
+    wa->setFocus(Qt::OtherFocusReason);
 	QObject::connect(wa, SIGNAL(titleChanged(GuiWorkArea *)),
 	                 this, SLOT(updateWindowTitle(GuiWorkArea *)));
 	QObject::connect(wa, SIGNAL(busy(bool)),
@@ -1736,7 +1746,7 @@ bool GuiView::event(QEvent * e)
 	case QEvent::WindowActivate: {
 		GuiView * old_view = guiApp->currentView();
 		if (this == old_view) {
-			setFocus();
+			setFocus(Qt::ActiveWindowFocusReason);
 			return QMainWindow::event(e);
 		}
 		if (old_view && old_view->currentBufferView()) {
@@ -1749,7 +1759,7 @@ bool GuiView::event(QEvent * e)
 			on_currentWorkAreaChanged(d.current_work_area_);
 		else
 			resetWindowTitle();
-		setFocus();
+		setFocus(Qt::ActiveWindowFocusReason);
 		return QMainWindow::event(e);
 	}
 
@@ -4836,6 +4846,7 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
 
 		case LFUN_DIALOG_HIDE: {
 			guiApp->hideDialogs(to_utf8(cmd.argument()), nullptr);
+            setFocus(Qt::PopupFocusReason);
 			break;
 		}
 
@@ -5361,7 +5372,10 @@ void GuiView::doShowDialog(QString const & qname, QString const & qdata,
 		Dialog * dialog = findOrBuild(name, false);
 		if (dialog) {
 			bool const visible = dialog->isVisibleView();
-			dialog->showData(sdata);
+			if (name == "findreplaceadv")
+				dialog->showData(sdata, Qt::OtherFocusReason);
+			else
+				dialog->showData(sdata);
 			if (currentBufferView())
 				currentBufferView()->editInset(name, inset);
 			// We only set the focus to the new dialog if it was not yet
diff --git a/src/frontends/qt/GuiView.h b/src/frontends/qt/GuiView.h
index ac6d293aaf..18a5672239 100644
--- a/src/frontends/qt/GuiView.h
+++ b/src/frontends/qt/GuiView.h
@@ -131,6 +131,7 @@ public:
 
 	///
 	void setFocus();
+	void setFocus(Qt::FocusReason reason);
 	bool hasFocus() const;
 
 	///
diff --git a/src/frontends/qt/GuiWorkArea.cpp b/src/frontends/qt/GuiWorkArea.cpp
index a4b874097c..4941d047cf 100644
--- a/src/frontends/qt/GuiWorkArea.cpp
+++ b/src/frontends/qt/GuiWorkArea.cpp
@@ -223,8 +223,14 @@ void GuiWorkArea::init()
 	// Enables input methods for asian languages.
 	// Must be set when creating custom text editing widgets.
 	setAttribute(Qt::WA_InputMethodEnabled, true);
-	// obtain transformation information to reset it when LyX gets refocus
-	d->im_item_trans_ = d->im_->inputItemTransform();
+
+	// 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() );
 }
 
 
@@ -283,6 +289,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
@@ -687,12 +696,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);
 }
@@ -1122,28 +1141,85 @@ 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_ - dim.width();
 	int cur_y = point.y_ + dim.height();
-	// 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 = 3;
-	// reset item transformation since it gets wrong after the item get
-	// lost and regain focus.
-	im_->setInputItemTransform(im_item_trans_);
+
+	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.
@@ -1783,7 +1859,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;
 }
@@ -1809,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;
 }
 
@@ -1833,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();
 
diff --git a/src/frontends/qt/GuiWorkArea.h b/src/frontends/qt/GuiWorkArea.h
index 6f24ff313c..148b79b73a 100644
--- a/src/frontends/qt/GuiWorkArea.h
+++ b/src/frontends/qt/GuiWorkArea.h
@@ -67,6 +67,8 @@ public:
 
 	/// return true if the key is part of a shortcut
 	bool queryKeySym(KeySymbol const & key, KeyModifier mod) const;
+	/// Ask relative position of input item coordinates against the main coordinates
+	void queryInputItemTransform();
 
 	bool inDialogMode() const;
 	void setDialogMode(bool mode);
diff --git a/src/frontends/qt/GuiWorkArea_Private.h b/src/frontends/qt/GuiWorkArea_Private.h
index 9da4cfaead..42dcef836c 100644
--- a/src/frontends/qt/GuiWorkArea_Private.h
+++ b/src/frontends/qt/GuiWorkArea_Private.h
@@ -100,6 +100,9 @@ struct GuiWorkArea::Private
 	/// Change the cursor when the mouse hovers over a clickable inset
 	void updateCursorShape();
 
+	/// Restore coordinate transformation information
+	void resetInputItemTransform();
+	/// Paint preedit text provided by the platform input method
 	void paintPreeditText(GuiPainter & pain);
 
 	/// Prepare screen for next painting
@@ -136,6 +139,7 @@ struct GuiWorkArea::Private
 	///
 	bool need_resize_ = false;
 
+	/// provides access to the platform input method
 	QInputMethod * im_ = QGuiApplication::inputMethod();
 	/// the current preedit text of the input method
 	docstring preedit_string_;
@@ -145,7 +149,10 @@ struct GuiWorkArea::Private
 	QList<QInputMethodEvent::Attribute> preedit_attr_;
 	QRectF im_cursor_rect_;
 	QRectF im_anchor_rect_;
-	QTransform im_item_trans_;
+	QTransform item_trans_;
+	bool item_trans_needs_reset_ = false;
+	/// for debug
+	QLocale::Language im_lang_;
 
 	/// Ratio between physical pixels and device-independent pixels
 	/// We save the last used value to detect changes of the


More information about the lyx-cvs mailing list