mirror of
https://github.com/logos-co/logos-basecamp.git
synced 2026-08-27 14:51:07 +00:00
402 lines
12 KiB
C++
402 lines
12 KiB
C++
// srcdeps: ShortcutBridge.cpp
|
|
//
|
|
// Unit tests for ShortcutBridge. Uses inline QML in a QQuickWidget as a
|
|
// stand-in for plugin panes. Run via `nix build .#unit-tests -L`.
|
|
|
|
#include "ShortcutBridge.h"
|
|
|
|
#include <QtTest/QtTest>
|
|
#include <QApplication>
|
|
#include <QLabel>
|
|
#include <QQmlComponent>
|
|
#include <QQmlEngine>
|
|
#include <QQuickItem>
|
|
#include <QQuickWidget>
|
|
#include <QShortcut>
|
|
#include <QSignalSpy>
|
|
#include <QStackedWidget>
|
|
#include <QVBoxLayout>
|
|
#include <QWidget>
|
|
|
|
namespace {
|
|
|
|
// Loads inline QML into a QQuickWidget and returns the widget.
|
|
QQuickWidget* makePane(const QString& qml, QWidget* parent = nullptr)
|
|
{
|
|
auto* qw = new QQuickWidget(parent);
|
|
qw->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
|
qw->setSource(QUrl(QStringLiteral("data:text/plain,")
|
|
+ qml.toUtf8().toPercentEncoding()));
|
|
QTest::qWait(50);
|
|
return qw;
|
|
}
|
|
|
|
QObject* findShortcutByObjectName(QQuickWidget* qw, const QString& name)
|
|
{
|
|
if (!qw || !qw->rootObject()) return nullptr;
|
|
return qw->rootObject()->findChild<QObject*>(name);
|
|
}
|
|
|
|
int mirrorCount(QWidget* host)
|
|
{
|
|
return host->findChildren<QShortcut*>().size();
|
|
}
|
|
|
|
// Let the bridge's QueuedConnection rebind hop settle.
|
|
void pump()
|
|
{
|
|
QCoreApplication::sendPostedEvents();
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
|
QCoreApplication::sendPostedEvents();
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class ShortcutBridgeTest : public QObject {
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
// -----------------------------------------------------------------
|
|
// Basic wiring
|
|
// -----------------------------------------------------------------
|
|
|
|
void mirrorsAreCreatedForCurrentPane()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* pane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item {\n"
|
|
" Shortcut { objectName: \"sc\"; sequence: \"Ctrl+K\" }\n"
|
|
"}\n",
|
|
stack);
|
|
stack->addWidget(pane);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
|
|
QCOMPARE(mirrorCount(&host), 1);
|
|
}
|
|
|
|
void multipleShortcutsAllMirrored()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* pane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item {\n"
|
|
" Shortcut { sequence: \"Ctrl+K\" }\n"
|
|
" Shortcut { sequence: \"Ctrl+F\" }\n"
|
|
" Shortcut { sequence: \"Ctrl+P\" }\n"
|
|
"}\n",
|
|
stack);
|
|
stack->addWidget(pane);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
|
|
QCOMPARE(mirrorCount(&host), 3);
|
|
}
|
|
|
|
void nonQuickPaneIsIgnored()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
stack->addWidget(new QLabel("plain widget", stack));
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
|
|
QCOMPARE(mirrorCount(&host), 0);
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Fire dispatch
|
|
// -----------------------------------------------------------------
|
|
|
|
void mirrorActivatedInvokesQmlHandler()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* pane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item {\n"
|
|
" id: root\n"
|
|
" property int hits: 0\n"
|
|
" Shortcut { objectName: \"sc\"; sequence: \"Ctrl+K\";\n"
|
|
" onActivated: root.hits = root.hits + 1 }\n"
|
|
"}\n",
|
|
stack);
|
|
stack->addWidget(pane);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
|
|
auto* mirror = host.findChild<QShortcut*>();
|
|
QVERIFY(mirror);
|
|
emit mirror->activated();
|
|
pump();
|
|
|
|
QCOMPARE(pane->rootObject()->property("hits").toInt(), 1);
|
|
}
|
|
|
|
// macOS first-press case: Qt fires activatedAmbiguously on the QML
|
|
// shortcut (registered first). Bridge must still run the handler.
|
|
void qmlAmbiguityInvokesQmlHandler()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* pane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item {\n"
|
|
" id: root\n"
|
|
" property int hits: 0\n"
|
|
" Shortcut { objectName: \"sc\"; sequence: \"Ctrl+K\";\n"
|
|
" onActivated: root.hits = root.hits + 1 }\n"
|
|
"}\n",
|
|
stack);
|
|
stack->addWidget(pane);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
|
|
QObject* qml = findShortcutByObjectName(pane, "sc");
|
|
QVERIFY(qml);
|
|
QMetaObject::invokeMethod(qml, "activatedAmbiguously");
|
|
pump();
|
|
|
|
QCOMPARE(pane->rootObject()->property("hits").toInt(), 1);
|
|
}
|
|
|
|
void disabledQmlShortcutIsNotFired()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* pane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item {\n"
|
|
" id: root\n"
|
|
" property int hits: 0\n"
|
|
" Shortcut { objectName: \"sc\"; sequence: \"Ctrl+K\";\n"
|
|
" enabled: false\n"
|
|
" onActivated: root.hits = root.hits + 1 }\n"
|
|
"}\n",
|
|
stack);
|
|
stack->addWidget(pane);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
|
|
auto* mirror = host.findChild<QShortcut*>();
|
|
QVERIFY(mirror);
|
|
emit mirror->activated();
|
|
pump();
|
|
|
|
QCOMPARE(pane->rootObject()->property("hits").toInt(), 0);
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Improvements #1-#4 from the design review
|
|
// -----------------------------------------------------------------
|
|
|
|
// Widget-scoped shortcuts stay item-local — must not be promoted
|
|
// to pane-wide via a mirror.
|
|
void widgetScopedShortcutsAreNotMirrored()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* pane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item {\n"
|
|
" Shortcut { sequence: \"Ctrl+A\"; context: Qt.WidgetShortcut }\n"
|
|
" Shortcut { sequence: \"Ctrl+B\"; context: Qt.WidgetWithChildrenShortcut }\n"
|
|
" Shortcut { sequence: \"Ctrl+K\"; context: Qt.WindowShortcut }\n"
|
|
" Shortcut { sequence: \"Ctrl+F\"; context: Qt.ApplicationShortcut }\n"
|
|
"}\n",
|
|
stack);
|
|
stack->addWidget(pane);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
|
|
// Only the two pane-wide / app-wide ones get mirrors.
|
|
QCOMPARE(mirrorCount(&host), 2);
|
|
}
|
|
|
|
// `Shortcut { sequences: [...] }` mirrors one QShortcut per alt.
|
|
void multiSequenceShortcutIsFullyMirrored()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* pane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item {\n"
|
|
" Shortcut { sequences: [\"Ctrl+K\", \"Ctrl+L\"] }\n"
|
|
"}\n",
|
|
stack);
|
|
stack->addWidget(pane);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
|
|
QCOMPARE(mirrorCount(&host), 2);
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Pane switching
|
|
// -----------------------------------------------------------------
|
|
|
|
void paneSwitchReplacesMirrors()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* paneA = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item {\n"
|
|
" Shortcut { sequence: \"Ctrl+A\" }\n"
|
|
" Shortcut { sequence: \"Ctrl+B\" }\n"
|
|
"}\n",
|
|
stack);
|
|
auto* paneB = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item {\n"
|
|
" Shortcut { sequence: \"Ctrl+X\" }\n"
|
|
"}\n",
|
|
stack);
|
|
stack->addWidget(paneA);
|
|
stack->addWidget(paneB);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
QCOMPARE(mirrorCount(&host), 2);
|
|
|
|
stack->setCurrentWidget(paneB);
|
|
pump();
|
|
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
|
QCOMPARE(mirrorCount(&host), 1);
|
|
|
|
stack->setCurrentWidget(paneA);
|
|
pump();
|
|
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
|
QCOMPARE(mirrorCount(&host), 2);
|
|
}
|
|
|
|
void hiddenPaneShortcutsAreNotMirrored()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* visiblePane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item { Shortcut { sequence: \"Ctrl+V\" } }\n",
|
|
stack);
|
|
auto* hiddenPane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item { Shortcut { sequence: \"Ctrl+K\" } }\n",
|
|
stack);
|
|
stack->addWidget(visiblePane);
|
|
stack->addWidget(hiddenPane);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
|
|
const auto mirrors = host.findChildren<QShortcut*>();
|
|
QCOMPARE(mirrors.size(), 1);
|
|
QCOMPARE(mirrors.first()->key(), QKeySequence("Ctrl+V"));
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Explicit rebind
|
|
// -----------------------------------------------------------------
|
|
|
|
void rebindDeferredPicksUpLateShortcuts()
|
|
{
|
|
QWidget host;
|
|
auto* layout = new QVBoxLayout(&host);
|
|
auto* stack = new QStackedWidget(&host);
|
|
layout->addWidget(stack);
|
|
|
|
auto* pane = makePane(
|
|
"import QtQuick 2.15\n"
|
|
"Item { objectName: \"root\" }\n",
|
|
stack);
|
|
stack->addWidget(pane);
|
|
host.show();
|
|
pump();
|
|
|
|
ShortcutBridge bridge(&host, stack);
|
|
pump();
|
|
QCOMPARE(mirrorCount(&host), 0);
|
|
|
|
// Splice a Shortcut in the way a Loader/dynamic Component would.
|
|
QQmlEngine* eng = pane->engine();
|
|
QQmlComponent comp(eng);
|
|
comp.setData(
|
|
"import QtQuick 2.15\n"
|
|
"Shortcut { sequence: \"Ctrl+K\" }\n",
|
|
QUrl("data:new-shortcut"));
|
|
QVERIFY2(!comp.isError(), qPrintable(comp.errorString()));
|
|
QObject* sc = comp.create(eng->rootContext());
|
|
QVERIFY(sc);
|
|
sc->setParent(pane->rootObject());
|
|
|
|
bridge.rebindDeferred();
|
|
pump();
|
|
|
|
QCOMPARE(mirrorCount(&host), 1);
|
|
}
|
|
};
|
|
|
|
QTEST_MAIN(ShortcutBridgeTest)
|
|
#include "shortcut_bridge_test.moc"
|