diff --git a/CMakeLists.txt b/CMakeLists.txt index c64f759..5ac306c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -find_package(Qt6 6.8 REQUIRED COMPONENTS Gui Quick Widgets Test WaylandClient) +find_package(Qt6 6.8 REQUIRED COMPONENTS Gui Quick Widgets Test DBus WaylandClient) find_package(LayerShellQt REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(XKB REQUIRED IMPORTED_TARGET xkbcommon) @@ -13,6 +13,7 @@ pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client) pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) qt_standard_project_setup(REQUIRES 6.8) +include(GNUInstallDirs) enable_testing() diff --git a/README.md b/README.md new file mode 100644 index 0000000..2847969 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# sloposk + +iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML UI. + +- 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 +- Auto-capitalization, double-space → ". ", backspace repeat → word delete +- Long-press space = trackpad; slide from `.?123` onto a symbol and release +- Floating: drag the bottom bar, resize from the corner; geometry is remembered per screen + +## Build & test + +```sh +cmake -B build -G Ninja && cmake --build build +ctest --test-dir build +``` + +Needs Qt ≥ 6.8 (Quick, WaylandClient, DBus, Test), LayerShellQt, xkbcommon, wayland-protocols. + +## Run + +KWin only offers the input-method protocol to the process it launches itself. + +| Where | How | +|---|---| +| Nested session (safe) | `kwin_wayland --no-lockscreen --socket sloposk-dev --width 1400 --height 900 --inputmethod $PWD/build/src/sloposk kwrite` | +| Desktop | `cmake --install build --prefix ~/.local`, then System Settings → Keyboard → Virtual Keyboard → **sloposk** | +| UI only | `build/src/sloposk` (stays visible, types nowhere) | + +Show/hide, e.g. from a global shortcut: `qdbus org.sloposk.Keyboard /Keyboard org.sloposk.Keyboard.Toggle` + +Dev hooks: `SLOPOSK_AUTOTYPE=text` commits on focus; `SLOPOSK_SCREENSHOT=file.png` saves the window and quits. + +## Layouts + +JSON in `layouts/` (built in) or `~/.local/share/sloposk/layouts/` (overrides by `id`). The globe key cycles them; the system locale's comes first. + +```json +{ + "id": "sv-SE", "name": "Svenska", "locale": "sv_SE", + "base": "en-US", + "layers": [{"name": "letters", "rows": [[ + "q", + {"label": "e", "flick": "3", "alternates": ["é", "è"]}, + {"type": "backspace", "width": 1.5} + ]]}] +} +``` + +- Key: a string, or an object with `type` (`char` `space` `backspace` `enter` `shift` `layer` `globe` `dismiss` `spacer`), `label`, `shifted`, `flick`, `alternates`, `width` (key units), `target` (for `layer`) +- `base`: take layers this layout doesn't define (e.g. numbers/symbols) from another layout +- Rows in a layer should add up to the same width; `tst_layoutloader` checks the built-ins + +## Architecture + +``` +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) +layout/ JSON → Layout → LayoutModel (geometry, hit testing) +window/ floating layer-shell placement, visibility, saved geometry +app/ D-Bus control, dev hooks +``` + +`sloposk_core` (everything compositor-independent, plus the `Sloposk` QML module) is a static library shared by the app and the tests. diff --git a/data/org.sloposk.keyboard.desktop b/data/org.sloposk.keyboard.desktop new file mode 100644 index 0000000..a7270a4 --- /dev/null +++ b/data/org.sloposk.keyboard.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=sloposk +Comment=iPad-style floating on-screen keyboard +Exec=sloposk +Type=Application +Icon=input-keyboard-virtual +NoDisplay=true +X-KDE-Wayland-VirtualKeyboard=true diff --git a/qml/Main.qml b/qml/Main.qml index ddeaa21..a818289 100644 --- a/qml/Main.qml +++ b/qml/Main.qml @@ -17,6 +17,8 @@ Window { width: 1000 height: 340 + popupMargin + minimumWidth: 480 + minimumHeight: 180 + popupMargin visible: false color: "transparent" @@ -42,5 +44,10 @@ Window { anchors { left: parent.left; right: parent.right; bottom: parent.bottom } onMoved: (dx, dy) => root.placement?.moveBy(dx, dy) } + + ResizeHandle { + anchors { right: parent.right; bottom: parent.bottom } + onResized: (dw, dh) => root.placement?.resizeBy(dw, dh) + } } } diff --git a/qml/ResizeHandle.qml b/qml/ResizeHandle.qml new file mode 100644 index 0000000..6ea7082 --- /dev/null +++ b/qml/ResizeHandle.qml @@ -0,0 +1,44 @@ +import QtQuick + +// Corner grip. The window grows from its top-left corner, so window +// coordinates stay put while resizing: report each step's delta. +Item { + id: grip + + signal resized(real dw, real dh) + + implicitWidth: 28 + implicitHeight: 28 + + // Two strokes across the corner, each from (w - d, h) to (w, h - d). + Repeater { + model: [9, 16] + Rectangle { + required property real modelData + readonly property real inset: 4 + + width: modelData * Math.SQRT2 + height: 2 + radius: 1 + color: Theme.handle + rotation: -45 + x: grip.width - inset - modelData / 2 - width / 2 + y: grip.height - inset - modelData / 2 - height / 2 + } + } + + DragHandler { + property point last + + target: null + cursorShape: Qt.SizeFDiagCursor + onActiveChanged: last = centroid.scenePressPosition + onCentroidChanged: { + if (!active) + return + const p = centroid.scenePosition + grip.resized(p.x - last.x, p.y - last.y) + last = p + } + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 36a0051..6d9952f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,6 +17,8 @@ qt_add_library(sloposk_core STATIC layout/LayoutRepository.h layout/LayoutRepository.cpp layout/KeyGeometry.h layout/KeyGeometry.cpp layout/LayoutModel.h layout/LayoutModel.cpp + window/VisibilityController.h window/VisibilityController.cpp + window/WindowSettings.h window/WindowSettings.cpp ) # QML lives in the top-level qml/ dir; alias each file so the module sees it at its root. @@ -26,6 +28,7 @@ set(SLOPOSK_QML_FILES Key.qml AlternatesPopup.qml DragHandle.qml + ResizeHandle.qml Theme.qml ) list(TRANSFORM SLOPOSK_QML_FILES PREPEND ${PROJECT_SOURCE_DIR}/qml/ OUTPUT_VARIABLE sloposk_qml_paths) @@ -59,7 +62,8 @@ target_link_libraries(sloposk_core PUBLIC Qt6::Quick PkgConfig::XKB) qt_add_executable(sloposk main.cpp - DevHooks.h DevHooks.cpp + app/DevHooks.h app/DevHooks.cpp + app/DBusControl.h app/DBusControl.cpp backend/InputMethodProtocol.h backend/KWinInputMethod.h backend/KWinInputMethod.cpp backend/KWinInputContext.h backend/KWinInputContext.cpp @@ -76,9 +80,11 @@ set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/qwayland-input-method-un target_link_libraries(sloposk PRIVATE sloposk_core sloposk_coreplugin + Qt6::DBus Qt6::WaylandClient LayerShellQt::Interface PkgConfig::WAYLAND ) install(TARGETS sloposk) +install(FILES ${PROJECT_SOURCE_DIR}/data/org.sloposk.keyboard.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) diff --git a/src/app/DBusControl.cpp b/src/app/DBusControl.cpp new file mode 100644 index 0000000..8518b93 --- /dev/null +++ b/src/app/DBusControl.cpp @@ -0,0 +1,22 @@ +#include "DBusControl.h" + +#include "window/VisibilityController.h" + +#include + +DBusControl::DBusControl(VisibilityController &visibility, QObject *parent) + : QObject(parent) + , m_visibility(visibility) +{ +} + +bool DBusControl::registerOnSessionBus() +{ + QDBusConnection bus = QDBusConnection::sessionBus(); + return bus.registerService(QStringLiteral("org.sloposk.Keyboard")) + && bus.registerObject(QStringLiteral("/Keyboard"), this, QDBusConnection::ExportScriptableSlots); +} + +void DBusControl::Show() { m_visibility.show(); } +void DBusControl::Hide() { m_visibility.hide(); } +void DBusControl::Toggle() { m_visibility.toggle(); } diff --git a/src/app/DBusControl.h b/src/app/DBusControl.h new file mode 100644 index 0000000..773d158 --- /dev/null +++ b/src/app/DBusControl.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +class VisibilityController; + +// Session-bus remote control, e.g. for a global shortcut: +// qdbus org.sloposk.Keyboard /Keyboard org.sloposk.Keyboard.Toggle +class DBusControl : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.sloposk.Keyboard") + +public: + explicit DBusControl(VisibilityController &visibility, QObject *parent = nullptr); + + // False if the bus is unavailable or another instance owns the name. + bool registerOnSessionBus(); + +public Q_SLOTS: + Q_SCRIPTABLE void Show(); + Q_SCRIPTABLE void Hide(); + Q_SCRIPTABLE void Toggle(); + +private: + VisibilityController &m_visibility; +}; diff --git a/src/DevHooks.cpp b/src/app/DevHooks.cpp similarity index 100% rename from src/DevHooks.cpp rename to src/app/DevHooks.cpp diff --git a/src/DevHooks.h b/src/app/DevHooks.h similarity index 100% rename from src/DevHooks.h rename to src/app/DevHooks.h diff --git a/src/main.cpp b/src/main.cpp index ed77a91..c89d786 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,13 @@ -#include "DevHooks.h" +#include "app/DBusControl.h" +#include "app/DevHooks.h" #include "backend/KWinInputMethod.h" #include "engine/DirectEngine.h" #include "input/KeyboardController.h" #include "layout/LayoutModel.h" #include "layout/LayoutRepository.h" #include "window/FloatingWindow.h" +#include "window/VisibilityController.h" +#include "window/WindowSettings.h" #include #include @@ -26,6 +29,36 @@ void loadLayouts(LayoutRepository &layouts) layouts.finalize(QLocale::system().name()); } +void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings) +{ + if (const auto geometry = settings.load(placement.screenName())) + placement.setGeometry(*geometry); + QObject::connect(&placement, &FloatingWindow::geometryChanged, &settings, + [&] { settings.save(placement.screenName(), placement.geometry()); }); +} + +void connectVisibility(VisibilityController &visibility, KWinInputMethod &backend, + KeyboardController &controller, FloatingWindow &placement) +{ + // 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()); }; + QObject::connect(&backend, &KWinInputMethod::availableChanged, &visibility, updateStandalone); + QObject::connect(&backend, &TextBackend::activeChanged, &visibility, + [&] { visibility.setFieldActive(backend.isActive()); }); + QObject::connect(&controller, &KeyboardController::dismissRequested, &visibility, &VisibilityController::hide); + QObject::connect(&visibility, &VisibilityController::visibleChanged, &placement, + [&](bool visible) { visible ? placement.show() : placement.hide(); }); + updateStandalone(); +} + +void connectTextContext(KWinInputMethod &backend, KeyboardController &controller) +{ + QObject::connect(&backend, &TextBackend::activeChanged, &controller, &KeyboardController::resetContext); + QObject::connect(&backend, &TextBackend::contextChanged, &controller, + [&] { controller.setContext(backend.context()); }); +} + } // namespace int main(int argc, char *argv[]) @@ -45,6 +78,7 @@ int main(int argc, char *argv[]) DirectEngine engine(backend); LayoutModel model; KeyboardController controller(engine, model, layouts); + connectTextContext(backend, controller); QQmlApplicationEngine qml; qml.setInitialProperties({ @@ -60,24 +94,16 @@ int main(int argc, char *argv[]) FloatingWindow placement(window); window->setProperty("placement", QVariant::fromValue(&placement)); + WindowSettings settings; + restoreAndPersistGeometry(placement, settings); - // Show while a text field is focused; with no input-method global (e.g. run - // standalone for UI work) stay visible so the keyboard can still be tried out. - auto updateVisibility = [&] { - if (backend.isActive() || !backend.isAvailable()) - placement.show(); - else - placement.hide(); - }; - QObject::connect(&backend, &TextBackend::activeChanged, &app, updateVisibility); - QObject::connect(&backend, &KWinInputMethod::availableChanged, &app, updateVisibility); - QObject::connect(&controller, &KeyboardController::dismissRequested, &placement, &FloatingWindow::hide); - QObject::connect(&backend, &TextBackend::activeChanged, &controller, &KeyboardController::resetContext); - QObject::connect(&backend, &TextBackend::contextChanged, &controller, - [&] { controller.setContext(backend.context()); }); - updateVisibility(); + VisibilityController visibility; + connectVisibility(visibility, backend, controller, placement); + + DBusControl dbus(visibility); + if (!dbus.registerOnSessionBus()) + qWarning("D-Bus control unavailable (is another instance running?)"); installDevHooks(backend, *window); - return app.exec(); } diff --git a/src/window/FloatingWindow.cpp b/src/window/FloatingWindow.cpp index dbc333c..cae9839 100644 --- a/src/window/FloatingWindow.cpp +++ b/src/window/FloatingWindow.cpp @@ -36,19 +36,44 @@ FloatingWindow::FloatingWindow(QWindow *window, QObject *parent) apply(); } -void FloatingWindow::setPosition(QPoint position) +QRect FloatingWindow::geometry() const { - position = clamped(position); - if (position == m_position) - return; - m_position = position; + return {m_position, m_window->size()}; +} + +void FloatingWindow::setGeometry(const QRect &geometry) +{ + const QSize size = geometry.size().expandedTo(m_window->minimumSize()).boundedTo(screenArea().size()); + m_window->resize(size); + m_position = clamped(geometry.topLeft(), size); apply(); - Q_EMIT positionChanged(); +} + +QString FloatingWindow::screenName() const +{ + return m_window->screen()->name(); } void FloatingWindow::moveBy(qreal dx, qreal dy) { - setPosition(m_position + QPoint(qRound(dx), qRound(dy))); + const QPoint position = clamped(m_position + QPoint(qRound(dx), qRound(dy)), m_window->size()); + if (position == m_position) + return; + m_position = position; + apply(); + Q_EMIT geometryChanged(); +} + +void FloatingWindow::resizeBy(qreal dw, qreal dh) +{ + const QSize maximum = screenArea().size() - QSize(m_position.x(), m_position.y()); + const QSize size = (m_window->size() + QSize(qRound(dw), qRound(dh))) + .expandedTo(m_window->minimumSize()) + .boundedTo(maximum); + if (size == m_window->size()) + return; + m_window->resize(size); + Q_EMIT geometryChanged(); } void FloatingWindow::setInputRegion(const QRectF ®ion) @@ -66,19 +91,24 @@ void FloatingWindow::hide() m_window->setVisible(false); } -QPoint FloatingWindow::clamped(QPoint position) const +QRect FloatingWindow::screenArea() const { - const QRect area = m_window->screen()->geometry(); - const int maxX = qMax(0, area.width() - m_window->width()); - const int maxY = qMax(0, area.height() - m_window->height()); + return m_window->screen()->geometry(); +} + +QPoint FloatingWindow::clamped(QPoint position, QSize size) const +{ + const QRect area = screenArea(); + const int maxX = qMax(0, area.width() - size.width()); + const int maxY = qMax(0, area.height() - size.height()); return {qBound(0, position.x(), maxX), qBound(0, position.y(), maxY)}; } QPoint FloatingWindow::defaultPosition() const { - const QRect area = m_window->screen()->geometry(); - return clamped({(area.width() - m_window->width()) / 2, - area.height() - m_window->height() - BottomGap}); + const QRect area = screenArea(); + return clamped({(area.width() - m_window->width()) / 2, area.height() - m_window->height() - BottomGap}, + m_window->size()); } void FloatingWindow::apply() @@ -86,5 +116,5 @@ void FloatingWindow::apply() if (m_layerSurface) m_layerSurface->setMargins({m_position.x(), m_position.y(), 0, 0}); else - m_window->setPosition(m_window->screen()->geometry().topLeft() + m_position); + m_window->setPosition(screenArea().topLeft() + m_position); } diff --git a/src/window/FloatingWindow.h b/src/window/FloatingWindow.h index 157114c..9bbf639 100644 --- a/src/window/FloatingWindow.h +++ b/src/window/FloatingWindow.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include class QWindow; @@ -10,29 +10,34 @@ namespace LayerShellQt { class Window; } // Places a keyboard window as a free-floating overlay that never takes focus. // On Wayland: a layer-shell surface anchored top-left, positioned via margins. // On X11: a frameless, non-focusable, always-on-top tool window. +// Geometry is relative to the window's screen and always kept on it. class FloatingWindow : public QObject { Q_OBJECT - Q_PROPERTY(QPoint position READ position WRITE setPosition NOTIFY positionChanged) public: // Must be called before the window is shown for the first time. explicit FloatingWindow(QWindow *window, QObject *parent = nullptr); - QPoint position() const { return m_position; } - void setPosition(QPoint position); + QRect geometry() const; + void setGeometry(const QRect &geometry); + QString screenName() const; Q_INVOKABLE void moveBy(qreal dx, qreal dy); + // Grows from the top-left corner; respects the window's minimum size. + Q_INVOKABLE void resizeBy(qreal dw, qreal dh); // Only this part of the window takes input; the rest passes through. Q_INVOKABLE void setInputRegion(const QRectF ®ion); Q_INVOKABLE void show(); Q_INVOKABLE void hide(); Q_SIGNALS: - void positionChanged(); + // Only for user-initiated changes (move/resize), not for restores. + void geometryChanged(); private: - QPoint clamped(QPoint position) const; + QRect screenArea() const; + QPoint clamped(QPoint position, QSize size) const; QPoint defaultPosition() const; void apply(); diff --git a/src/window/VisibilityController.cpp b/src/window/VisibilityController.cpp new file mode 100644 index 0000000..301e4ad --- /dev/null +++ b/src/window/VisibilityController.cpp @@ -0,0 +1,43 @@ +#include "VisibilityController.h" + +void VisibilityController::setFieldActive(bool active) +{ + if (active == m_fieldActive) + return; + m_fieldActive = active; + m_override.reset(); + update(); +} + +void VisibilityController::setStandalone(bool standalone) +{ + m_standalone = standalone; + update(); +} + +void VisibilityController::show() +{ + m_override = true; + update(); +} + +void VisibilityController::hide() +{ + m_override = false; + update(); +} + +void VisibilityController::toggle() +{ + m_override = !m_visible; + update(); +} + +void VisibilityController::update() +{ + const bool visible = m_override.value_or(m_standalone || m_fieldActive); + if (visible == m_visible) + return; + m_visible = visible; + Q_EMIT visibleChanged(visible); +} diff --git a/src/window/VisibilityController.h b/src/window/VisibilityController.h new file mode 100644 index 0000000..5aad089 --- /dev/null +++ b/src/window/VisibilityController.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +// Decides whether the keyboard is shown: +// - while a text field is focused +// - always, when running standalone (no input-method connection) +// - show()/hide()/toggle() override that until the focus changes +class VisibilityController : public QObject +{ + Q_OBJECT + +public: + using QObject::QObject; + + bool isVisible() const { return m_visible; } + + void setFieldActive(bool active); + void setStandalone(bool standalone); + + void show(); + void hide(); + void toggle(); + +Q_SIGNALS: + void visibleChanged(bool visible); + +private: + void update(); + + bool m_fieldActive = false; + bool m_standalone = false; + std::optional m_override; + bool m_visible = false; +}; diff --git a/src/window/WindowSettings.cpp b/src/window/WindowSettings.cpp new file mode 100644 index 0000000..867b5a3 --- /dev/null +++ b/src/window/WindowSettings.cpp @@ -0,0 +1,52 @@ +#include "WindowSettings.h" + +#include + +namespace { + +constexpr int DebounceMs = 500; + +QString key(const QString &screen) +{ + // Screen names may contain '/', which QSettings treats as a group separator. + return QStringLiteral("geometry/") + QString(screen).replace(u'/', u'_'); +} + +} // namespace + +WindowSettings::WindowSettings(QObject *parent) + : QObject(parent) +{ + m_debounce.setSingleShot(true); + m_debounce.setInterval(DebounceMs); + connect(&m_debounce, &QTimer::timeout, this, &WindowSettings::flush); +} + +WindowSettings::~WindowSettings() +{ + if (m_debounce.isActive()) + flush(); +} + +std::optional WindowSettings::load(const QString &screen) const +{ + const QRect geometry = QSettings().value(key(screen)).toRect(); + if (!geometry.isValid()) + return std::nullopt; + return geometry; +} + +void WindowSettings::save(const QString &screen, const QRect &geometry) +{ + if (m_debounce.isActive() && screen != m_pendingScreen) + flush(); + m_pendingScreen = screen; + m_pendingGeometry = geometry; + m_debounce.start(); +} + +void WindowSettings::flush() +{ + m_debounce.stop(); + QSettings().setValue(key(m_pendingScreen), m_pendingGeometry); +} diff --git a/src/window/WindowSettings.h b/src/window/WindowSettings.h new file mode 100644 index 0000000..29987e0 --- /dev/null +++ b/src/window/WindowSettings.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +// Remembers the keyboard's geometry per screen (~/.config/sloposk.conf). +// Saving is debounced so dragging doesn't hammer the disk. +class WindowSettings : public QObject +{ + Q_OBJECT + +public: + explicit WindowSettings(QObject *parent = nullptr); + ~WindowSettings() override; + + std::optional load(const QString &screen) const; + void save(const QString &screen, const QRect &geometry); + +private: + void flush(); + + QTimer m_debounce; + QString m_pendingScreen; + QRect m_pendingGeometry; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7170fe3..9bc7de7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,6 +13,7 @@ sloposk_add_test(tst_pointertracker) sloposk_add_test(tst_alternatespicker) sloposk_add_test(tst_textrules) sloposk_add_test(tst_keyrepeater) +sloposk_add_test(tst_visibility) # Needs the QML module itself, not just the C++ backing library. sloposk_add_test(tst_keyboardsurface) diff --git a/tests/tst_visibility.cpp b/tests/tst_visibility.cpp new file mode 100644 index 0000000..f9fe7ac --- /dev/null +++ b/tests/tst_visibility.cpp @@ -0,0 +1,48 @@ +#include "window/VisibilityController.h" + +#include + +class TestVisibility : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void followsFieldFocus() + { + VisibilityController v; + v.setFieldActive(true); + QVERIFY(v.isVisible()); + v.setFieldActive(false); + QVERIFY(!v.isVisible()); + } + + void dismissLastsUntilFocusChanges() + { + VisibilityController v; + v.setFieldActive(true); + v.hide(); + QVERIFY(!v.isVisible()); + v.setFieldActive(false); + v.setFieldActive(true); + QVERIFY(v.isVisible()); + } + + void toggleShowsWithoutField() + { + VisibilityController v; + v.toggle(); + QVERIFY(v.isVisible()); + v.toggle(); + QVERIFY(!v.isVisible()); + } + + void standaloneAlwaysVisible() + { + VisibilityController v; + v.setStandalone(true); + QVERIFY(v.isVisible()); + } +}; + +QTEST_GUILESS_MAIN(TestVisibility) +#include "tst_visibility.moc"