inputmethod: Ensure InputPanelV1Window is always within the screen

Currently when input panel is using overlay mode and the cursor rectangle
is below or above the screen area, the input panel may be placed off the
screen. The change ensure it is always placed within the screen area
using similar math like xdg_popup's slide_y constrain.
This commit is contained in:
Weng Xuetian 2024-01-10 22:37:51 -08:00
parent 4f512c6571
commit 1fd5a6555e
No known key found for this signature in database
GPG key ID: 8E8B898CBF2412F9
4 changed files with 121 additions and 6 deletions

View file

@ -10,6 +10,7 @@
#include "core/output.h"
#include "inputmethod.h"
#include "inputpanelv1window.h"
#include "keyboard_input.h"
#include "pointer_input.h"
#include "qwayland-input-method-unstable-v1.h"
@ -63,6 +64,8 @@ private Q_SLOTS:
void testDisableShowInputPanel();
void testModifierForwarding();
void testFakeEventFallback();
void testOverlayPositioning_data();
void testOverlayPositioning();
private:
void touchNow()
@ -552,6 +555,10 @@ void InputMethodTest::testV3Styling()
// Merged range should be [1,6).
QCOMPARE(textInputPreeditSpy.last().at(1), 1);
QCOMPARE(textInputPreeditSpy.last().at(2), 6);
shellSurface.reset();
QVERIFY(Test::waitForWindowClosed(window));
QVERIFY(!kwinApp()->inputMethod()->isActive());
}
void InputMethodTest::testDisableShowInputPanel()
@ -584,6 +591,9 @@ void InputMethodTest::testDisableShowInputPanel()
textInputV2->showInputPanel();
QVERIFY(requestShowInputPanelSpy.count() || requestShowInputPanelSpy.wait());
QVERIFY(!kwinApp()->inputMethod()->isActive());
shellSurface.reset();
QVERIFY(Test::waitForWindowClosed(window));
}
void InputMethodTest::testModifierForwarding()
@ -655,6 +665,10 @@ void InputMethodTest::testModifierForwarding()
QVERIFY(modifierSpy.count() == 3 || modifierSpy.wait());
disconnect(keyChangedConnection);
disconnect(modifiersChangedConnection);
shellSurface.reset();
QVERIFY(Test::waitForWindowClosed(window));
QVERIFY(!kwinApp()->inputMethod()->isActive());
}
void InputMethodTest::testFakeEventFallback()
@ -726,6 +740,71 @@ void InputMethodTest::testFakeEventFallback()
compare(keySpy.at(0), KEY_ENTER, KWayland::Client::Keyboard::KeyState::Pressed);
compare(keySpy.at(1), KEY_ENTER, KWayland::Client::Keyboard::KeyState::Released);
shellSurface.reset();
QVERIFY(Test::waitForWindowClosed(window));
kwinApp()->inputMethod()->setActive(false);
QVERIFY(!kwinApp()->inputMethod()->isActive());
}
void InputMethodTest::testOverlayPositioning_data()
{
QTest::addColumn<QRect>("cursorRectangle");
QTest::addColumn<QRect>("result");
QTest::newRow("regular") << QRect(10, 20, 30, 40) << QRect(60, 160, 200, 50);
QTest::newRow("offscreen-left") << QRect(-200, 40, 30, 40) << QRect(0, 180, 200, 50);
QTest::newRow("offscreen-right") << QRect(1200, 40, 30, 40) << QRect(1080, 180, 200, 50);
QTest::newRow("offscreen-top") << QRect(1200, -400, 30, 40) << QRect(1080, 0, 200, 50);
// Check it is flipped near the bottom of screen (anchor point 844 + 100 + 40 = 1024 - 40)
QTest::newRow("offscreen-bottom-flip") << QRect(1200, 844, 30, 40) << QRect(1080, 894, 200, 50);
// Top is (screen height 1024 - window height 50) = 984
QTest::newRow("offscreen-bottom-slide") << QRect(1200, 1200, 30, 40) << QRect(1080, 974, 200, 50);
}
void InputMethodTest::testOverlayPositioning()
{
QFETCH(QRect, cursorRectangle);
QFETCH(QRect, result);
Test::inputMethod()->setMode(Test::MockInputMethod::Mode::Overlay);
QVERIFY(!kwinApp()->inputMethod()->isActive());
touchNow();
QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved);
QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
std::unique_ptr<KWayland::Client::TextInput> textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
// Create an xdg_toplevel surface and wait for the compositor to catch up.
std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
// Make the window smaller than the screen and move it.
Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1080, 824), Qt::red);
window->move(QPointF(50, 100));
waylandServer()->seat()->setFocusedTextInputSurface(window->surface());
textInput->setCursorRectangle(cursorRectangle);
textInput->enable(surface.get());
// Overlay is shown upon activate
QVERIFY(windowAddedSpy.wait());
QCOMPARE(workspace()->activeWindow(), window);
QCOMPARE(windowAddedSpy.count(), 2);
QVERIFY(activateSpy.count() || activateSpy.wait());
QVERIFY(kwinApp()->inputMethod()->isActive());
auto keyboardWindow = kwinApp()->inputMethod()->panel();
QVERIFY(keyboardWindow);
// Check the overlay window is placed with cursor rectangle + window position.
QCOMPARE(keyboardWindow->frameGeometry(), result);
// Destroy the test window.
shellSurface.reset();
QVERIFY(Test::waitForWindowClosed(window));
Test::inputMethod()->setMode(Test::MockInputMethod::Mode::TopLevel);
}
WAYLANDTEST_MAIN(InputMethodTest)

View file

@ -446,6 +446,11 @@ class MockInputMethod : public QObject, QtWayland::zwp_input_method_v1
{
Q_OBJECT
public:
enum class Mode {
TopLevel,
Overlay,
};
MockInputMethod(struct wl_registry *registry, int id, int version);
~MockInputMethod();
@ -458,6 +463,8 @@ public:
return m_context;
}
void setMode(Mode mode);
Q_SIGNALS:
void activate();
@ -469,6 +476,7 @@ private:
std::unique_ptr<KWayland::Client::Surface> m_inputSurface;
QtWayland::zwp_input_panel_surface_v1 *m_inputMethodSurface = nullptr;
struct ::zwp_input_method_context_v1 *m_context = nullptr;
Mode m_mode = Mode::TopLevel;
};
class FractionalScaleManagerV1 : public QObject, public QtWayland::wp_fractional_scale_manager_v1
@ -683,7 +691,8 @@ enum class CreationSetup {
};
QtWayland::zwp_input_panel_surface_v1 *createInputPanelSurfaceV1(KWayland::Client::Surface *surface,
KWayland::Client::Output *output);
KWayland::Client::Output *output,
MockInputMethod::Mode mode);
FractionalScaleV1 *createFractionalScaleV1(KWayland::Client::Surface *surface);

View file

@ -326,14 +326,27 @@ void MockInputMethod::zwp_input_method_v1_activate(struct ::zwp_input_method_con
{
if (!m_inputSurface) {
m_inputSurface = Test::createSurface();
m_inputMethodSurface = Test::createInputPanelSurfaceV1(m_inputSurface.get(), s_waylandConnection.outputs.first());
m_inputMethodSurface = Test::createInputPanelSurfaceV1(m_inputSurface.get(), s_waylandConnection.outputs.first(), m_mode);
}
m_context = context;
Test::render(m_inputSurface.get(), QSize(1280, 400), Qt::blue);
switch (m_mode) {
case Mode::TopLevel:
Test::render(m_inputSurface.get(), QSize(1280, 400), Qt::blue);
break;
case Mode::Overlay:
Test::render(m_inputSurface.get(), QSize(200, 50), Qt::blue);
break;
}
Q_EMIT activate();
}
void MockInputMethod::setMode(MockInputMethod::Mode mode)
{
m_mode = mode;
}
void MockInputMethod::zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context)
{
QCOMPARE(context, m_context);
@ -924,7 +937,7 @@ LayerSurfaceV1 *createLayerSurfaceV1(KWayland::Client::Surface *surface, const Q
return shellSurface;
}
QtWayland::zwp_input_panel_surface_v1 *createInputPanelSurfaceV1(KWayland::Client::Surface *surface, KWayland::Client::Output *output)
QtWayland::zwp_input_panel_surface_v1 *createInputPanelSurfaceV1(KWayland::Client::Surface *surface, KWayland::Client::Output *output, MockInputMethod::Mode mode)
{
if (!s_waylandConnection.inputPanelV1) {
qWarning() << "Unable to create the input panel surface. The interface input_panel global is not bound";
@ -937,7 +950,14 @@ QtWayland::zwp_input_panel_surface_v1 *createInputPanelSurfaceV1(KWayland::Clien
return nullptr;
}
s->set_toplevel(output->output(), QtWayland::zwp_input_panel_surface_v1::position_center_bottom);
switch (mode) {
case MockInputMethod::Mode::TopLevel:
s->set_toplevel(output->output(), QtWayland::zwp_input_panel_surface_v1::position_center_bottom);
break;
case MockInputMethod::Mode::Overlay:
s->set_overlay_panel();
break;
}
return s;
}

View file

@ -140,10 +140,17 @@ void InputPanelV1Window::reposition()
QRectF(popupOffset(cursorRectangle, Qt::TopEdge | Qt::LeftEdge, Qt::RightEdge | Qt::TopEdge, m_windowGeometry.size()), m_windowGeometry.size());
// if it still doesn't fit we should continue with the unflipped version
if (flippedPopupRect.top() >= screen.top() || flippedPopupRect.bottom() <= screen.bottom()) {
if (flippedPopupRect.top() >= screen.top() && flippedPopupRect.bottom() <= screen.bottom()) {
popupRect.moveTop(flippedPopupRect.top());
}
}
if (popupRect.top() < screen.top()) {
popupRect.moveTop(screen.top());
}
if (popupRect.bottom() > screen.bottom()) {
popupRect.moveBottom(screen.bottom());
}
moveResize(popupRect);
}
} break;