Type into any window: fake input when no text field is focused

- KWinFakeInput (org_kde_kwin_fake_input v6, offered by KWin to the
  input-method process): keysyms with real modifier keys held
- The KWin backend uses the input method in text fields and fake input
  everywhere else, so shortcuts, terminals and the desktop get keys
- Text rules (auto-capitalization, double-space) only apply in fields
- Always Show setting keeps the keyboard up without a text field

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 21:59:32 +02:00
co-authored by Claude Opus 5.5
parent ba2f17d665
commit f816135162
20 changed files with 303 additions and 23 deletions
+5 -2
View File
@@ -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.
+2
View File
@@ -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
+103
View File
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="fake_input">
<copyright><![CDATA[
SPDX-FileCopyrightText: 2015 Martin Gräßlin
SPDX-License-Identifier: LGPL-2.1-or-later
]]></copyright>
<interface name="org_kde_kwin_fake_input" version="6">
<description summary="Fake input manager">
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.
</description>
<request name="authenticate">
<description summary="Information why the client wants to use the interface">
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.
</description>
<arg name="application" type="string" summary="user visible name of the application"/>
<arg name="reason" type="string" summary="reason why the application wants to use this interface"/>
</request>
<request name="pointer_motion">
<arg name="delta_x" type="fixed"/>
<arg name="delta_y" type="fixed"/>
</request>
<request name="button">
<arg name="button" type="uint"/>
<arg name="state" type="uint"/>
</request>
<request name="axis">
<arg name="axis" type="uint"/>
<arg name="value" type="fixed"/>
</request>
<request name="touch_down" since="2">
<description summary="touch down event">
A client should use this request to send touch down event at specific
coordinates.
</description>
<arg name="id" type="uint" summary="unique id for touch down event"/>
<arg name="x" type="fixed" summary="x coordinate for touch down event"/>
<arg name="y" type="fixed" summary="y coordinate for touch down event"/>
</request>
<request name="touch_motion" since="2">
<description summary="touch motion event">
A client should use this request to send touch motion to specific position.
</description>
<arg name="id" type="uint" summary="unique id for touch motion event"/>
<arg name="x" type="fixed" summary="x coordinate for touch motion event"/>
<arg name="y" type="fixed" summary="y coordinate for touch motion event"/>
</request>
<request name="touch_up" since="2">
<description summary="touch up event">
A client should use this request to send touch up event.
</description>
<arg name="id" type="uint" summary="unique id for touch up event"/>
</request>
<request name="touch_cancel" since="2">
<description summary="touch cancel event">
A client should use this request to cancel the current
touch event.
</description>
</request>
<request name="touch_frame" since="2">
<description summary="touch frame event">
A client should use this request to send touch frame event.
</description>
</request>
<request name="pointer_motion_absolute" since="3">
<arg name="x" type="fixed"/>
<arg name="y" type="fixed"/>
</request>
<request name="keyboard_key" since="4">
<arg name="button" type="uint"/>
<arg name="state" type="uint"/>
</request>
<!-- Version 5 additions -->
<request name="destroy" type="destructor" since="5">
<description summary="Destroy the fake input device"/>
</request>
<!-- Version 6 additions -->
<request name="keyboard_keysym" since="6">
<arg name="keysym" type="uint"/>
<arg name="state" type="uint"/>
</request>
</interface>
</protocol>
+8
View File
@@ -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")
+2
View File
@@ -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
+60
View File
@@ -0,0 +1,60 @@
#include "KWinFakeInput.h"
#include <xkbcommon/xkbcommon.h>
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));
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <QWaylandClientExtensionTemplate>
#include <qnamespace.h>
#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<KWinFakeInput>, 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);
};
+1
View File
@@ -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();
}
+13 -1
View File
@@ -1,6 +1,8 @@
#include "KWinInputMethod.h"
#include "KWinInputContext.h"
#include <xkbcommon/xkbcommon-keysyms.h>
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)
+5
View File
@@ -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<KWinInputContext> m_context;
};
+2 -1
View File
@@ -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); }
+2 -3
View File
@@ -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)
+3 -2
View File
@@ -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);
+7 -3
View File
@@ -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())
+11
View File
@@ -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)
+6
View File
@@ -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;
};
+8 -1
View File
@@ -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;
+4 -1
View File
@@ -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<bool> m_override;
bool m_visible = false;
};
+25 -9
View File
@@ -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<KeyboardController>(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;
+12
View File
@@ -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;