diff --git a/README.md b/README.md index 7fab61a..62b2379 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML U - Flick a key down for its secondary symbol; long-press for accented alternates - iOS shift: tap = one letter, double-tap = caps lock, hold = shift while held +- Works everywhere, not just in text fields: with none focused, keys reach the focused window like a hardware keyboard (shortcuts, terminals, the desktop); "Always Show" in ⚙ keeps it up - Letters follow the system keyboard layout; the globe key switches it - Auto-capitalization, double-space → ". " (both optional, ⚙ on the bottom bar), backspace repeat → word delete - Long-press space = trackpad; slide from `.?123` onto a symbol and release @@ -60,7 +61,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso ## Settings -`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `layout/visibleSections` (`main` plus section ids), `layout/fullKeyboard`. +`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `layout/visibleSections` (`main` plus section ids), `layout/fullKeyboard`, `window/alwaysVisible`. ## Architecture @@ -68,7 +69,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso qml/ view only input/ touch → PointerTracker gestures → KeyboardController rules engine/ key output → text ops (DirectEngine; prediction/swipe later) -backend/ text ops → compositor (KWin input-method-v1); the compositor's keymap +backend/ text ops → compositor (KWin input-method-v1, or fake input with no text field); the compositor's keymap layout/ template JSON + keymap letters → Layout → LayoutModel (geometry, hit testing) window/ floating layer-shell placement, visibility, saved geometry app/ D-Bus control and system layouts, dev hooks @@ -76,3 +77,5 @@ settings/ persisted user preferences ``` `sloposk_core` (everything compositor-independent, plus the `Sloposk` QML module) is a static library shared by the app and the tests. + +`protocols/fake-input.xml` is KDE's `org_kde_kwin_fake_input` (LGPL-2.1-or-later), vendored from plasma-wayland-protocols. diff --git a/data/org.sloposk.keyboard.desktop b/data/org.sloposk.keyboard.desktop index a7270a4..e12d0d4 100644 --- a/data/org.sloposk.keyboard.desktop +++ b/data/org.sloposk.keyboard.desktop @@ -6,3 +6,5 @@ Type=Application Icon=input-keyboard-virtual NoDisplay=true X-KDE-Wayland-VirtualKeyboard=true +# Key events for windows without a text field (fake input is restricted). +X-KDE-Wayland-Interfaces=org_kde_kwin_fake_input diff --git a/protocols/fake-input.xml b/protocols/fake-input.xml new file mode 100644 index 0000000..33734c0 --- /dev/null +++ b/protocols/fake-input.xml @@ -0,0 +1,103 @@ + + + + + + This interface allows other processes to provide fake input events. + Purpose is on the one hand side to provide testing facilities like XTest on X11. + But also to support use case like kdeconnect's mouse pad interface. + + A compositor should not trust the input received from this interface. + Clients should not expect that the compositor honors the requests from this + interface. + + Warning! The protocol described in this file is a desktop environment + implementation detail. Regular clients must not use this protocol. + Backward incompatible changes may be added without bumping the major + version of the extension. + + + + A client should use this request to tell the compositor why it wants to + use this interface. The compositor might use the information to decide + whether it wants to grant the request. The data might also be passed to + the user to decide whether the application should get granted access to + this very privileged interface. + + + + + + + + + + + + + + + + + + + A client should use this request to send touch down event at specific + coordinates. + + + + + + + + A client should use this request to send touch motion to specific position. + + + + + + + + A client should use this request to send touch up event. + + + + + + A client should use this request to cancel the current + touch event. + + + + + A client should use this request to send touch frame event. + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qml/SettingsPanel.qml b/qml/SettingsPanel.qml index 6c54c2b..efc29c0 100644 --- a/qml/SettingsPanel.qml +++ b/qml/SettingsPanel.qml @@ -32,6 +32,14 @@ Rectangle { onToggled: checked => panel.settings.autoCapitalize = checked } + ToggleRow { + width: parent.width + text: qsTr("Always Show") + detail: qsTr("Type into any window, not just text fields") + checked: panel.settings.alwaysVisible + onToggled: checked => panel.settings.alwaysVisible = checked + } + ToggleRow { width: parent.width text: qsTr("“.” Shortcut") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6b3f034..65d15dd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -78,12 +78,14 @@ qt_add_executable(sloposk backend/InputMethodProtocol.h backend/KWinInputMethod.h backend/KWinInputMethod.cpp backend/KWinInputContext.h backend/KWinInputContext.cpp + backend/KWinFakeInput.h backend/KWinFakeInput.cpp backend/WaylandKeymap.h backend/WaylandKeymap.cpp window/FloatingWindow.h window/FloatingWindow.cpp ) qt6_generate_wayland_protocol_client_sources(sloposk PRIVATE_CODE FILES ${WAYLAND_PROTOCOLS_DIR}/unstable/input-method/input-method-unstable-v1.xml + ${PROJECT_SOURCE_DIR}/protocols/fake-input.xml ) # Generated code lacks the core-protocol include (see backend/InputMethodProtocol.h). set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/qwayland-input-method-unstable-v1.cpp diff --git a/src/backend/KWinFakeInput.cpp b/src/backend/KWinFakeInput.cpp new file mode 100644 index 0000000..5e58a50 --- /dev/null +++ b/src/backend/KWinFakeInput.cpp @@ -0,0 +1,60 @@ +#include "KWinFakeInput.h" + +#include + +namespace { + +constexpr uint32_t Released = 0; +constexpr uint32_t Pressed = 1; + +// Modifiers are held with real keys (evdev codes) so they combine with any keysym. +struct ModifierKey { + Qt::KeyboardModifier flag; + uint32_t evdevCode; +}; +constexpr ModifierKey ModifierKeys[] = { + {Qt::ControlModifier, 29}, // KEY_LEFTCTRL + {Qt::ShiftModifier, 42}, // KEY_LEFTSHIFT + {Qt::AltModifier, 56}, // KEY_LEFTALT + {Qt::MetaModifier, 125}, // KEY_LEFTMETA +}; + +} // namespace + +KWinFakeInput::KWinFakeInput() + : QWaylandClientExtensionTemplate(6) // 6: keyboard_keysym +{ + connect(this, &QWaylandClientExtension::activeChanged, this, [this] { + if (isActive()) + authenticate(QStringLiteral("sloposk"), QStringLiteral("On-screen keyboard: type into any window")); + }); + initialize(); +} + +KWinFakeInput::~KWinFakeInput() +{ + if (isActive()) + destroy(); +} + +void KWinFakeInput::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) +{ + if (!isActive()) + return; + for (const ModifierKey &key : ModifierKeys) { + if (modifiers & key.flag) + keyboard_key(key.evdevCode, Pressed); + } + keyboard_keysym(keysym, Pressed); + keyboard_keysym(keysym, Released); + for (const ModifierKey &key : ModifierKeys) { + if (modifiers & key.flag) + keyboard_key(key.evdevCode, Released); + } +} + +void KWinFakeInput::typeText(const QString &text) +{ + for (const char32_t ucs : text.toUcs4()) + sendKeysym(xkb_utf32_to_keysym(ucs)); +} diff --git a/src/backend/KWinFakeInput.h b/src/backend/KWinFakeInput.h new file mode 100644 index 0000000..7acb1a6 --- /dev/null +++ b/src/backend/KWinFakeInput.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include "qwayland-fake-input.h" + +// Key events for whatever window has focus, as from a hardware keyboard: +// KWin's fake-input protocol, which it offers to the input-method process. +// Used when no text field is focused, so shortcuts, terminals and games work. +class KWinFakeInput : public QWaylandClientExtensionTemplate, public QtWayland::org_kde_kwin_fake_input +{ + Q_OBJECT + +public: + KWinFakeInput(); + ~KWinFakeInput() override; + + // Presses and releases `keysym` with `modifiers` held. KWin finds the key + // (and any shift level) in the current keymap. + void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier); + // Types each character as its keysym. + void typeText(const QString &text); +}; diff --git a/src/backend/KWinInputContext.cpp b/src/backend/KWinInputContext.cpp index 274b466..e9789f9 100644 --- a/src/backend/KWinInputContext.cpp +++ b/src/backend/KWinInputContext.cpp @@ -40,6 +40,7 @@ KWinInputContext::KWinInputContext(::zwp_input_method_context_v1 *object, QObjec : QObject(parent) , QtWayland::zwp_input_method_context_v1(object) { + m_context.field = true; announceModifiers(); } diff --git a/src/backend/KWinInputMethod.cpp b/src/backend/KWinInputMethod.cpp index 88ac945..a2a4bab 100644 --- a/src/backend/KWinInputMethod.cpp +++ b/src/backend/KWinInputMethod.cpp @@ -1,6 +1,8 @@ #include "KWinInputMethod.h" #include "KWinInputContext.h" +#include + KWinInputMethodGlobal::KWinInputMethodGlobal() : QWaylandClientExtensionTemplate(1) { @@ -46,12 +48,20 @@ void KWinInputMethod::commitText(const QString &text) { if (m_context) m_context->commit(text); + else + m_fakeInput.typeText(text); } void KWinInputMethod::replaceBeforeCursor(const QString &before, const QString &text) { - if (m_context) + if (m_context) { m_context->replaceBeforeCursor(before, text); + return; + } + // Key events are one ordered stream, so plain Backspaces are safe here. + for (qsizetype i = 0; i < before.toUcs4().size(); ++i) + m_fakeInput.sendKeysym(XKB_KEY_BackSpace); + m_fakeInput.typeText(text); } void KWinInputMethod::setPreedit(const QString &text) @@ -64,6 +74,8 @@ void KWinInputMethod::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers { if (m_context) m_context->sendKeysym(keysym, modifiers); + else + m_fakeInput.sendKeysym(keysym, modifiers); } void KWinInputMethod::activate(::zwp_input_method_context_v1 *context) diff --git a/src/backend/KWinInputMethod.h b/src/backend/KWinInputMethod.h index d4acc2a..b2999ca 100644 --- a/src/backend/KWinInputMethod.h +++ b/src/backend/KWinInputMethod.h @@ -1,5 +1,6 @@ #pragma once +#include "KWinFakeInput.h" #include "TextBackend.h" #include "InputMethodProtocol.h" @@ -28,6 +29,9 @@ protected: void zwp_input_method_v1_deactivate(::zwp_input_method_context_v1 *context) override; }; +// Types into the focused text field through the input method; with none +// focused, sends key events to the focused window through fake input, so the +// keyboard works everywhere (shortcuts, terminals, the desktop). class KWinInputMethod : public TextBackend { Q_OBJECT @@ -55,5 +59,6 @@ private: void deactivate(); KWinInputMethodGlobal m_global; + KWinFakeInput m_fakeInput; std::unique_ptr m_context; }; diff --git a/src/backend/TextContext.h b/src/backend/TextContext.h index a7d570b..b59a146 100644 --- a/src/backend/TextContext.h +++ b/src/backend/TextContext.h @@ -23,6 +23,7 @@ struct TextContext { Multiline = 0x200, }; + bool field = false; // a text field is focused; without one, keys go to the window as from a keyboard bool hasSurroundingText = false; QString textBeforeCursor; Purpose purpose = Purpose::Normal; @@ -33,7 +34,7 @@ struct TextContext { { const bool prosePurpose = purpose == Purpose::Normal || purpose == Purpose::Alpha || purpose == Purpose::Name; - return prosePurpose && !(hints & (HiddenText | SensitiveData)); + return field && prosePurpose && !(hints & (HiddenText | SensitiveData)); } bool allowsAutoCapitalization() const { return isProse() && !(hints & Lowercase); } diff --git a/src/input/KeyboardController.cpp b/src/input/KeyboardController.cpp index 1cb298a..8ae6643 100644 --- a/src/input/KeyboardController.cpp +++ b/src/input/KeyboardController.cpp @@ -141,12 +141,11 @@ void KeyboardController::moveCursor(QPoint steps) repeat(qMax(0, -steps.y()), XKB_KEY_Up); } -void KeyboardController::resetContext() +void KeyboardController::resetContext(const TextContext &context) { m_deadKeys.cancel(); - m_context = {}; m_textBefore.clear(); - autoCapitalize(); + setContext(context); } void KeyboardController::setContext(const TextContext &context) diff --git a/src/input/KeyboardController.h b/src/input/KeyboardController.h index ae76a5d..92f83f5 100644 --- a/src/input/KeyboardController.h +++ b/src/input/KeyboardController.h @@ -53,8 +53,9 @@ public: // Space-bar trackpad: move the text cursor by whole steps. void moveCursor(QPoint steps); - // A field was focused or unfocused: forget what we knew about the text. - void resetContext(); + // A field was focused (context.field) or unfocused: forget what we knew + // about the text and start over from what it reported. + void resetContext(const TextContext &context); // The focused field reported new text/content type. void setContext(const TextContext &context); void setOptions(const TypingOptions &options); diff --git a/src/main.cpp b/src/main.cpp index 9e8fe37..e05f918 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -85,9 +85,12 @@ void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settin [&] { settings.save(placement.screenName(), placement.geometry()); }); } -void connectVisibility(VisibilityController &visibility, KWinInputMethod &backend, +void connectVisibility(VisibilityController &visibility, KWinInputMethod &backend, const Settings &settings, KeyboardController &controller, FloatingWindow &placement) { + visibility.setAlwaysVisible(settings.alwaysVisible()); + QObject::connect(&settings, &Settings::alwaysVisibleChanged, &visibility, + [&] { visibility.setAlwaysVisible(settings.alwaysVisible()); }); // Without the input-method global (run outside KWin's control, e.g. for UI // work) there is no focus signal: stay visible so it can be tried out. const auto updateStandalone = [&] { visibility.setStandalone(!backend.isAvailable()); }; @@ -102,7 +105,8 @@ void connectVisibility(VisibilityController &visibility, KWinInputMethod &backen void connectTextContext(KWinInputMethod &backend, KeyboardController &controller) { - QObject::connect(&backend, &TextBackend::activeChanged, &controller, &KeyboardController::resetContext); + QObject::connect(&backend, &TextBackend::activeChanged, &controller, + [&] { controller.resetContext(backend.context()); }); QObject::connect(&backend, &TextBackend::contextChanged, &controller, [&] { controller.setContext(backend.context()); }); } @@ -158,7 +162,7 @@ int main(int argc, char *argv[]) connectKeySettings(settings, model, placement, *window, rebuildLayout); VisibilityController visibility; - connectVisibility(visibility, backend, controller, placement); + connectVisibility(visibility, backend, settings, controller, placement); DBusControl dbus(visibility); if (!dbus.registerOnSessionBus()) diff --git a/src/settings/Settings.cpp b/src/settings/Settings.cpp index 1370cb5..40f6c3b 100644 --- a/src/settings/Settings.cpp +++ b/src/settings/Settings.cpp @@ -9,6 +9,7 @@ constexpr auto AutoCapitalizeKey = "typing/autoCapitalize"; constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod"; constexpr auto VisibleSectionsKey = "layout/visibleSections"; constexpr auto FullKeyboardKey = "layout/fullKeyboard"; +constexpr auto AlwaysVisibleKey = "window/alwaysVisible"; } Settings::Settings(QObject *parent) @@ -22,6 +23,16 @@ Settings::Settings(QObject *parent) if (m_visibleSections.isEmpty()) m_visibleSections = {MainSectionId}; m_fullKeyboard = store.value(FullKeyboardKey, false).toBool(); + m_alwaysVisible = store.value(AlwaysVisibleKey, false).toBool(); +} + +void Settings::setAlwaysVisible(bool always) +{ + if (always == m_alwaysVisible) + return; + m_alwaysVisible = always; + QSettings().setValue(AlwaysVisibleKey, always); + Q_EMIT alwaysVisibleChanged(); } void Settings::setFullKeyboard(bool full) diff --git a/src/settings/Settings.h b/src/settings/Settings.h index e7af02a..d2a2e3b 100644 --- a/src/settings/Settings.h +++ b/src/settings/Settings.h @@ -19,6 +19,8 @@ class Settings : public QObject Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged) // iPad Pro-style full keyboard (number row, tab, ctrl/alt/super) instead of the compact one. Q_PROPERTY(bool fullKeyboard READ fullKeyboard WRITE setFullKeyboard NOTIFY fullKeyboardChanged) + // Shown even without a focused text field, to type into any window. + Q_PROPERTY(bool alwaysVisible READ alwaysVisible WRITE setAlwaysVisible NOTIFY alwaysVisibleChanged) public: explicit Settings(QObject *parent = nullptr); @@ -36,11 +38,14 @@ public: bool fullKeyboard() const { return m_fullKeyboard; } void setFullKeyboard(bool full); + bool alwaysVisible() const { return m_alwaysVisible; } + void setAlwaysVisible(bool always); Q_SIGNALS: void typingOptionsChanged(); void visibleSectionsChanged(); void fullKeyboardChanged(); + void alwaysVisibleChanged(); private: void setTypingOptions(const TypingOptions &options); @@ -48,4 +53,5 @@ private: TypingOptions m_typing; QStringList m_visibleSections; bool m_fullKeyboard = false; + bool m_alwaysVisible = false; }; diff --git a/src/window/VisibilityController.cpp b/src/window/VisibilityController.cpp index 301e4ad..2384c05 100644 --- a/src/window/VisibilityController.cpp +++ b/src/window/VisibilityController.cpp @@ -15,6 +15,13 @@ void VisibilityController::setStandalone(bool standalone) update(); } +void VisibilityController::setAlwaysVisible(bool always) +{ + m_alwaysVisible = always; + m_override.reset(); + update(); +} + void VisibilityController::show() { m_override = true; @@ -35,7 +42,7 @@ void VisibilityController::toggle() void VisibilityController::update() { - const bool visible = m_override.value_or(m_standalone || m_fieldActive); + const bool visible = m_override.value_or(m_alwaysVisible || m_standalone || m_fieldActive); if (visible == m_visible) return; m_visible = visible; diff --git a/src/window/VisibilityController.h b/src/window/VisibilityController.h index 5aad089..19e48bd 100644 --- a/src/window/VisibilityController.h +++ b/src/window/VisibilityController.h @@ -5,7 +5,8 @@ // Decides whether the keyboard is shown: // - while a text field is focused -// - always, when running standalone (no input-method connection) +// - always, if the user wants it (to type into any window) or when running +// standalone (no input-method connection) // - show()/hide()/toggle() override that until the focus changes class VisibilityController : public QObject { @@ -18,6 +19,7 @@ public: void setFieldActive(bool active); void setStandalone(bool standalone); + void setAlwaysVisible(bool always); void show(); void hide(); @@ -31,6 +33,7 @@ private: bool m_fieldActive = false; bool m_standalone = false; + bool m_alwaysVisible = false; std::optional m_override; bool m_visible = false; }; diff --git a/tests/tst_controller.cpp b/tests/tst_controller.cpp index f927734..e347ec3 100644 --- a/tests/tst_controller.cpp +++ b/tests/tst_controller.cpp @@ -10,6 +10,10 @@ using namespace Qt::StringLiterals; namespace { +const TextContext Field{.field = true}; +// A field with some text already: no auto-shift until a test asks for it. +const TextContext MidSentence{.field = true, .hasSurroundingText = true, .textBeforeCursor = u"so"_s}; + KeyDef makeKey(KeyType type, const QString &label = {}) { KeyDef k; @@ -44,6 +48,7 @@ private Q_SLOTS: model.setLayout(TestLayouts::build("us")); model.setVisibleSections({MainSectionId}); controller = std::make_unique(engine, model); + controller->resetContext(MidSentence); // most tests type into a text field } void typesLowercase() @@ -103,7 +108,7 @@ private Q_SLOTS: void capitalizesAtSentenceStart() { - controller->resetContext(); + controller->resetContext(Field); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); for (const char *c : {"h", "i", ".", " "}) tap(makeKey(KeyType::Character, QString::fromLatin1(c))); @@ -114,7 +119,7 @@ private Q_SLOTS: void noCapitalizationInPasswordFields() { - TextContext context; + TextContext context = Field; context.purpose = TextContext::Purpose::Password; controller->setContext(context); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); @@ -122,7 +127,7 @@ private Q_SLOTS: void fieldTextDrivesCapitalization() { - TextContext context; + TextContext context = Field; context.hasSurroundingText = true; context.textBeforeCursor = u"Done! "_s; controller->setContext(context); @@ -152,12 +157,12 @@ private Q_SLOTS: tap(makeKey(KeyType::Character, u"a"_s)); tap(space); tap(space); - QCOMPARE(engine.output, u"A "); + QCOMPARE(engine.output, u"a "); } void autoCapitalizationCanBeTurnedOff() { - controller->resetContext(); + controller->resetContext(Field); controller->setOptions({.autoCapitalize = false}); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); tap(makeKey(KeyType::Character, u"a"_s)); @@ -166,7 +171,7 @@ private Q_SLOTS: void doubleTapShiftLocksAtSentenceStart() { - controller->resetContext(); // a field was focused + controller->resetContext(Field); // a field was focused QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); // auto-shift const KeyDef shift = makeKey(KeyType::Shift); controller->keyDown(shift, 5000); @@ -281,7 +286,7 @@ private Q_SLOTS: void autoShiftAndCapsLockDontApplyToFunctionKeys() { - controller->resetContext(); // sentence start: auto-shift on + controller->resetContext(Field); // sentence start: auto-shift on QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); tap(keysymKey(XKB_KEY_Left)); QCOMPARE(engine.lastModifiers, Qt::NoModifier); @@ -303,10 +308,10 @@ private Q_SLOTS: lj.shifted = u"2"_s; KeyDef one = makeKey(KeyType::Character, u"+"_s); one.shifted = u"1"_s; - controller->resetContext(); // sentence start + controller->resetContext(Field); // sentence start tap(one); QCOMPARE(engine.output, u"+"); // not the shifted level - controller->resetContext(); + controller->resetContext(Field); tap(lj); QCOMPARE(engine.output, u"+Ľ"); tap(makeKey(KeyType::Shift)); // the user's shift does pick it @@ -325,6 +330,17 @@ private Q_SLOTS: QVERIFY(engine.output.endsWith(u"é"_s)); } + void noTextRulesWithoutAField() + { + controller->resetContext({}); // keys go to a window, not a text field + QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); + const KeyDef space = makeKey(KeyType::Space); + tap(makeKey(KeyType::Character, u"a"_s)); + tap(space); + tap(space); + QCOMPARE(engine.output, u"a "); + } + void dismissEmitsSignal() { int dismissed = 0; diff --git a/tests/tst_visibility.cpp b/tests/tst_visibility.cpp index f9fe7ac..a823e48 100644 --- a/tests/tst_visibility.cpp +++ b/tests/tst_visibility.cpp @@ -27,6 +27,18 @@ private Q_SLOTS: QVERIFY(v.isVisible()); } + void alwaysVisibleShowsWithoutField() + { + VisibilityController v; + v.setAlwaysVisible(true); + QVERIFY(v.isVisible()); + v.hide(); // dismiss key still works... + QVERIFY(!v.isVisible()); + v.setFieldActive(true); // ...until the focus changes + v.setFieldActive(false); + QVERIFY(v.isVisible()); + } + void toggleShowsWithoutField() { VisibilityController v;