aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--CursorNavigation.pro6
-rw-r--r--DemoApplication/CNButton.qml17
-rw-r--r--DemoApplication/DemoApplication.pro27
-rw-r--r--DemoApplication/HomeForm.qml14
-rw-r--r--DemoApplication/Page1Form.qml74
-rw-r--r--DemoApplication/Page2Form.qml14
-rw-r--r--DemoApplication/main.cpp17
-rw-r--r--DemoApplication/main.qml66
-rw-r--r--DemoApplication/qml.qrc10
-rw-r--r--DemoApplication/qtquickcontrols2.conf6
-rw-r--r--README.md0
-rw-r--r--plugin/cursornavigation.cpp87
-rw-r--r--plugin/cursornavigation.h45
-rw-r--r--plugin/cursornavigationalgorithm.cpp13
-rw-r--r--plugin/cursornavigationalgorithm.h26
-rw-r--r--plugin/cursornavigationattached.cpp82
-rw-r--r--plugin/cursornavigationattached.h54
-rw-r--r--plugin/inputadapter.cpp83
-rw-r--r--plugin/inputadapter.h40
-rw-r--r--plugin/inputtypes.cpp23
-rw-r--r--plugin/inputtypes.h33
-rw-r--r--plugin/itemregister.cpp36
-rw-r--r--plugin/itemregister.h30
-rw-r--r--plugin/plugin.cpp13
-rw-r--r--plugin/plugin.h16
-rw-r--r--plugin/plugin.pro36
-rw-r--r--plugin/qmldir2
-rw-r--r--plugin/spatialnavigation4dir.cpp161
-rw-r--r--plugin/spatialnavigation4dir.h17
-rw-r--r--tests/tests.pro10
-rw-r--r--tests/tst_cursornavigation.cpp58
32 files changed, 1118 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..72587da
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.user
+*.qmlc
diff --git a/CursorNavigation.pro b/CursorNavigation.pro
new file mode 100644
index 0000000..e76e405
--- /dev/null
+++ b/CursorNavigation.pro
@@ -0,0 +1,6 @@
+TEMPLATE = subdirs
+
+SUBDIRS += \
+ DemoApplication \
+ plugin \
+ tests
diff --git a/DemoApplication/CNButton.qml b/DemoApplication/CNButton.qml
new file mode 100644
index 0000000..edd44cf
--- /dev/null
+++ b/DemoApplication/CNButton.qml
@@ -0,0 +1,17 @@
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import CursorNavigation 1.0
+
+Button {
+ id: button
+ CursorNavigation.acceptsCursor: true
+ property bool hasCursor: CursorNavigation.hasCursor
+
+ Rectangle {
+ border.width: 2
+ border.color: "red"
+ anchors.fill: parent
+ visible: button.hasCursor
+ }
+
+}
diff --git a/DemoApplication/DemoApplication.pro b/DemoApplication/DemoApplication.pro
new file mode 100644
index 0000000..6ad5a8e
--- /dev/null
+++ b/DemoApplication/DemoApplication.pro
@@ -0,0 +1,27 @@
+QT += quick
+CONFIG += c++11
+
+# The following define makes your compiler emit warnings if you use
+# any feature of Qt which as been marked deprecated (the exact warnings
+# depend on your compiler). Please consult the documentation of the
+# deprecated API in order to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+TARGET = demo
+DESTDIR = ..
+
+# You can also make your code fail to compile if you use deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+
+SOURCES += \
+ main.cpp
+
+RESOURCES += qml.qrc
+
+# Additional import path used to resolve QML modules in Qt Creator's code model
+QML_IMPORT_PATH =
+
+# Additional import path used to resolve QML modules just for Qt Quick Designer
+QML_DESIGNER_IMPORT_PATH =
diff --git a/DemoApplication/HomeForm.qml b/DemoApplication/HomeForm.qml
new file mode 100644
index 0000000..83412a6
--- /dev/null
+++ b/DemoApplication/HomeForm.qml
@@ -0,0 +1,14 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+
+Page {
+ width: 600
+ height: 400
+
+ title: qsTr("Home")
+
+ Label {
+ text: qsTr("You are on the home page.")
+ anchors.centerIn: parent
+ }
+}
diff --git a/DemoApplication/Page1Form.qml b/DemoApplication/Page1Form.qml
new file mode 100644
index 0000000..cd2007c
--- /dev/null
+++ b/DemoApplication/Page1Form.qml
@@ -0,0 +1,74 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+
+Page {
+ width: 600
+ height: 400
+
+ title: qsTr("Page 1")
+
+ Label {
+ text: qsTr("You are on Page 1.")
+ anchors.centerIn: parent
+ }
+
+ CNButton {
+ id: button
+ x: 52
+ y: 50
+ text: qsTr("Button")
+ }
+
+ CNButton {
+ id: button1
+ x: 110
+ y: 138
+ text: qsTr("Button")
+ }
+
+ CNButton {
+ id: button2
+ x: 202
+ y: 241
+ text: qsTr("Button")
+ }
+
+ CNButton {
+ id: button3
+ x: 383
+ y: 241
+ text: qsTr("Button")
+ }
+
+ CNButton {
+ id: button4
+ x: 383
+ y: 322
+ text: qsTr("Button")
+ }
+
+ CNButton {
+ id: button5
+ x: 383
+ y: 138
+ text: qsTr("Button")
+ }
+
+ CNButton {
+ id: button6
+ x: 383
+ y: 50
+ text: qsTr("Button")
+ }
+
+ CNButton {
+ id: button7
+ x: 62
+ y: 241
+ text: qsTr("Button")
+ }
+
+
+
+}
diff --git a/DemoApplication/Page2Form.qml b/DemoApplication/Page2Form.qml
new file mode 100644
index 0000000..34b9dc6
--- /dev/null
+++ b/DemoApplication/Page2Form.qml
@@ -0,0 +1,14 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+
+Page {
+ width: 600
+ height: 400
+
+ title: qsTr("Page 2")
+
+ Label {
+ text: qsTr("You are on Page 2.")
+ anchors.centerIn: parent
+ }
+}
diff --git a/DemoApplication/main.cpp b/DemoApplication/main.cpp
new file mode 100644
index 0000000..0635536
--- /dev/null
+++ b/DemoApplication/main.cpp
@@ -0,0 +1,17 @@
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication::addLibraryPath(QLatin1String("./plugin"));
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+
+ QGuiApplication app(argc, argv);
+
+ QQmlApplicationEngine engine;
+ engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
+ if (engine.rootObjects().isEmpty())
+ return -1;
+
+ return app.exec();
+}
diff --git a/DemoApplication/main.qml b/DemoApplication/main.qml
new file mode 100644
index 0000000..685e8dc
--- /dev/null
+++ b/DemoApplication/main.qml
@@ -0,0 +1,66 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+import CursorNavigation 1.0
+
+ApplicationWindow {
+ id: window
+ visible: true
+ width: 640
+ height: 480
+ title: qsTr("Stack")
+
+ header: ToolBar {
+ contentHeight: toolButton.implicitHeight
+
+ ToolButton {
+ id: toolButton
+ text: stackView.depth > 1 ? "\u25C0" : "\u2630"
+ font.pixelSize: Qt.application.font.pixelSize * 1.6
+ onClicked: {
+ if (stackView.depth > 1) {
+ stackView.pop()
+ } else {
+ drawer.open()
+ }
+ }
+ }
+
+ Label {
+ text: stackView.currentItem.title
+ anchors.centerIn: parent
+ }
+ }
+
+ Drawer {
+ id: drawer
+ width: window.width * 0.66
+ height: window.height
+
+ Column {
+ anchors.fill: parent
+
+ ItemDelegate {
+ text: qsTr("Page 1")
+ width: parent.width
+ onClicked: {
+ stackView.push("Page1Form.qml")
+ drawer.close()
+ }
+ }
+ ItemDelegate {
+ text: qsTr("Page 2")
+ width: parent.width
+ onClicked: {
+ stackView.push("Page2Form.qml")
+ drawer.close()
+ }
+ }
+ }
+ }
+
+ StackView {
+ id: stackView
+ initialItem: "HomeForm.qml"
+ anchors.fill: parent
+ }
+}
diff --git a/DemoApplication/qml.qrc b/DemoApplication/qml.qrc
new file mode 100644
index 0000000..dbc7738
--- /dev/null
+++ b/DemoApplication/qml.qrc
@@ -0,0 +1,10 @@
+<RCC>
+ <qresource prefix="/">
+ <file>main.qml</file>
+ <file>HomeForm.qml</file>
+ <file>Page1Form.qml</file>
+ <file>Page2Form.qml</file>
+ <file>qtquickcontrols2.conf</file>
+ <file>CNButton.qml</file>
+ </qresource>
+</RCC>
diff --git a/DemoApplication/qtquickcontrols2.conf b/DemoApplication/qtquickcontrols2.conf
new file mode 100644
index 0000000..75b2cb8
--- /dev/null
+++ b/DemoApplication/qtquickcontrols2.conf
@@ -0,0 +1,6 @@
+; This file can be edited to change the style of the application
+; Read "Qt Quick Controls 2 Configuration File" for details:
+; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html
+
+[Controls]
+Style=Default
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/README.md
diff --git a/plugin/cursornavigation.cpp b/plugin/cursornavigation.cpp
new file mode 100644
index 0000000..5cb9545
--- /dev/null
+++ b/plugin/cursornavigation.cpp
@@ -0,0 +1,87 @@
+#include "cursornavigation.h"
+#include "cursornavigationalgorithm.h"
+#include "spatialnavigation4dir.h"
+#include <QQuickWindow>
+#include <QQuickItem>
+
+const char CursorNavigation::windowPropertyName[] = "cursor_navigation";
+
+CursorNavigation::CursorNavigation(QQuickWindow *parent)
+:QObject(parent)
+,m_inputAdapter(parent, this)
+,m_currentItem(nullptr)
+{
+ m_algorithms.push_back(new SpatialNavigation4Dir(&m_itemRegister));
+}
+
+bool CursorNavigation::inputCommand(CursorNavigationCommand cmd)
+{
+ QQuickItem *nextItem;
+
+ for (auto alg : m_algorithms) {
+ nextItem = alg->getNextCandidate(m_itemRegister.items(), m_currentItem, cmd);
+ if (nextItem)
+ break;
+ }
+
+ if (nextItem) {
+ if (m_currentItem) {
+ CursorNavigationAttached *current=cursorNavigationAttachment(m_currentItem);
+ Q_ASSERT(current);
+ current->setHasCursor(false);
+ }
+ CursorNavigationAttached *next=cursorNavigationAttachment(nextItem);
+ Q_ASSERT(next);
+ next->setHasCursor(true);
+ m_currentItem = nextItem;
+ }
+}
+
+CursorNavigationAttached *CursorNavigation::qmlAttachedProperties(QObject *object)
+{
+ // if the object is a window, use its contentItem instead
+ if (auto win = qobject_cast<QQuickWindow *>(object)) {
+ object = win->contentItem();
+ }
+
+ if (!qobject_cast<QQuickItem *>(object)) {
+ qWarning("Cannot manage focus for a non-Item!");
+ return nullptr;
+ }
+
+ QQuickItem *item = static_cast<QQuickItem *>(object);
+
+ // TODO: what if an object, with an already attached object, gets reparented (say, in another window?)
+ // with or without a focus system.
+
+ return new CursorNavigationAttached(item);
+}
+
+CursorNavigation *CursorNavigation::cursorNavigationForWindow(QQuickWindow *window)
+{
+ if (!window)
+ return nullptr;
+
+ const QVariant &oldCursorNavigation = window->property(windowPropertyName);
+ if (!oldCursorNavigation.isNull())
+ return oldCursorNavigation.value<CursorNavigation *>();
+
+ CursorNavigation *cursorNavigation = new CursorNavigation(window);
+ window->setProperty(windowPropertyName, QVariant::fromValue(cursorNavigation));
+
+ //why would the context property be needed?
+ /*if (QQmlEngine *engine = cn->qmlEngine(window)) {
+ engine->rootContext()->setContextProperty("_cursorNavigation", cn);
+ } else {
+ qDebug() << "Couldn't find QQmlEngine";
+ }*/
+
+ return cursorNavigation;
+}
+
+CursorNavigationAttached *CursorNavigation::cursorNavigationAttachment(QQuickItem *item)
+{
+ Q_ASSERT(item);
+ return static_cast<CursorNavigationAttached *>(qmlAttachedPropertiesObject<CursorNavigation>(item, false));
+}
+
diff --git a/plugin/cursornavigation.h b/plugin/cursornavigation.h
new file mode 100644
index 0000000..a9309fb
--- /dev/null
+++ b/plugin/cursornavigation.h
@@ -0,0 +1,45 @@
+#ifndef CURSORNAVIGATION_H
+#define CURSORNAVIGATION_H
+
+#include "cursornavigationattached.h"
+#include "itemregister.h"
+#include "inputtypes.h"
+#include "inputadapter.h"
+
+#include <QObject>
+#include <qqml.h>
+
+class QQuickItem;
+class CursorNavigationAlgorithm;
+
+class CursorNavigation : public QObject
+{
+ Q_OBJECT
+
+public:
+ CursorNavigation(QQuickWindow *parent);
+
+ bool inputCommand(CursorNavigationCommand cmd);
+
+ static CursorNavigationAttached *qmlAttachedProperties(QObject *object);
+
+ static CursorNavigation *cursorNavigationForWindow(QQuickWindow *window);
+
+private:
+ void setCursorOnItem(QQuickItem *item);
+ static CursorNavigationAttached *cursorNavigationAttachment(QQuickItem *item);
+
+private:
+ static const char windowPropertyName[];
+ InputAdapter m_inputAdapter;
+ QQuickItem *m_currentItem; //item that currently has the cursor
+ QList<CursorNavigationAlgorithm*> m_algorithms;
+ ItemRegister m_itemRegister;
+
+
+ friend class CursorNavigationAttached;
+};
+
+QML_DECLARE_TYPEINFO(CursorNavigation, QML_HAS_ATTACHED_PROPERTIES)
+
+#endif // CURSORNAVIGATION_H
diff --git a/plugin/cursornavigationalgorithm.cpp b/plugin/cursornavigationalgorithm.cpp
new file mode 100644
index 0000000..d21c81c
--- /dev/null
+++ b/plugin/cursornavigationalgorithm.cpp
@@ -0,0 +1,13 @@
+#include "cursornavigationalgorithm.h"
+#include "itemregister.h"
+
+CursorNavigationAlgorithm::CursorNavigationAlgorithm(ItemRegister *itemRegister)
+:m_itemRegister(itemRegister)
+{
+
+}
+
+CursorNavigationAlgorithm::~CursorNavigationAlgorithm()
+{
+
+}
diff --git a/plugin/cursornavigationalgorithm.h b/plugin/cursornavigationalgorithm.h
new file mode 100644
index 0000000..02f247a
--- /dev/null
+++ b/plugin/cursornavigationalgorithm.h
@@ -0,0 +1,26 @@
+#ifndef CURSORNAVIGATIONALGORITHM_H
+#define CURSORNAVIGATIONALGORITHM_H
+
+#include <QList>
+#include "inputtypes.h"
+
+class ItemRegister;
+class QQuickItem;
+
+class CursorNavigationAlgorithm
+{
+public:
+ CursorNavigationAlgorithm(ItemRegister *itemRegister);
+
+ virtual ~CursorNavigationAlgorithm();
+
+ virtual QQuickItem* getNextCandidate(const QList<QQuickItem*> &candidates,
+ const QQuickItem *currentItem,
+ const CursorNavigationCommand& cmd) = 0;
+
+private:
+ ItemRegister *m_itemRegister;
+
+};
+
+#endif // CURSORNAVIGATIONALGORITHM_H
diff --git a/plugin/cursornavigationattached.cpp b/plugin/cursornavigationattached.cpp
new file mode 100644
index 0000000..50b3686
--- /dev/null
+++ b/plugin/cursornavigationattached.cpp
@@ -0,0 +1,82 @@
+#include "cursornavigationattached.h"
+#include "cursornavigation.h"
+#include <QQuickItem>
+#include <QQuickWindow>
+
+CursorNavigationAttached::CursorNavigationAttached(QQuickItem *parent)
+:QObject(parent),
+m_acceptsCursor(true),
+m_cursorNavigation(nullptr),
+m_hasCursor(false)
+{
+ connect(parent, &QQuickItem::windowChanged, this, &CursorNavigationAttached::onWindowChanged);
+
+ if (item()->window())
+ {
+ qDebug() << "Item has a window already";
+ onWindowChanged(item()->window());
+ }
+}
+
+bool CursorNavigationAttached::acceptsCursor() const
+{
+ return m_acceptsCursor;
+}
+
+void CursorNavigationAttached::setAcceptsCursor(bool acceptsCursor)
+{
+ if (acceptsCursor != m_acceptsCursor) {
+ m_acceptsCursor=acceptsCursor;
+ emit acceptsCursorChanged(m_acceptsCursor);
+ }
+}
+
+bool CursorNavigationAttached::hasCursor() const
+{
+ return m_hasCursor;
+}
+
+bool CursorNavigationAttached::trapsCursor() const
+{
+ return m_trapsCursor;
+}
+
+void CursorNavigationAttached::setTrapsCursor(bool trapsCursor)
+{
+ if (trapsCursor != m_trapsCursor) {
+ m_trapsCursor=trapsCursor;
+ emit trapsCursorChanged(m_trapsCursor);
+ }
+}
+
+void CursorNavigationAttached::onWindowChanged(QQuickWindow *window)
+{
+ qDebug() << "window changed, window = " << window;
+ if (m_cursorNavigation)
+ m_cursorNavigation->m_itemRegister.unregisterItem(item());
+
+ if (window) {
+ m_cursorNavigation = CursorNavigation::cursorNavigationForWindow(window);
+ } else {
+ m_cursorNavigation = nullptr;
+ }
+
+ if (m_cursorNavigation)
+ m_cursorNavigation->m_itemRegister.registerItem(item());
+
+ //emit focusManagerChanged();
+}
+
+QQuickItem *CursorNavigationAttached::item() const
+{
+ Q_ASSERT(qobject_cast<QQuickItem *>(parent()));
+ return static_cast<QQuickItem *>(parent());
+}
+
+void CursorNavigationAttached::setHasCursor(bool hasCursor)
+{
+ if (hasCursor != m_hasCursor) {
+ m_hasCursor=hasCursor;
+ emit hasCursorChanged(m_hasCursor);
+ }
+}
diff --git a/plugin/cursornavigationattached.h b/plugin/cursornavigationattached.h
new file mode 100644
index 0000000..2be0f72
--- /dev/null
+++ b/plugin/cursornavigationattached.h
@@ -0,0 +1,54 @@
+#ifndef CURSORNAVIGATIONATTACHED_H
+#define CURSORNAVIGATIONATTACHED_H
+
+//#include <qqml.h>
+#include <QObject>
+
+class CursorNavigation;
+class QQuickItem;
+class QQuickWindow;
+
+class CursorNavigationAttached : public QObject
+{
+ Q_OBJECT
+ //is available for cursor navigation
+ Q_PROPERTY(bool acceptsCursor READ acceptsCursor WRITE setAcceptsCursor NOTIFY acceptsCursorChanged)
+ //is available for cursor navigation
+ Q_PROPERTY(bool hasCursor READ hasCursor NOTIFY hasCursorChanged)
+ //traps cursor. a trapped cursor can not be traversed outside of the item that traps it
+ Q_PROPERTY(bool trapsCursor READ trapsCursor WRITE setTrapsCursor NOTIFY trapsCursorChanged)
+ //proxy cursor to other items
+ //Q_PROPERTY(QQmlListProperty<QQuickItem> preferredCursorTargets READ preferredCursorTargetsQML)
+
+public:
+ CursorNavigationAttached(QQuickItem *parent);
+
+ bool acceptsCursor() const;
+ void setAcceptsCursor(bool acceptsCursor);
+
+ bool hasCursor() const;
+
+ bool trapsCursor() const;
+ void setTrapsCursor(bool trapsCursor);
+
+signals:
+ void acceptsCursorChanged(bool acceptsCursor);
+ void hasCursorChanged(bool hasCursor);
+ void trapsCursorChanged(bool trapsCursor);
+
+private slots:
+ void onWindowChanged(QQuickWindow *window);
+
+private:
+ QQuickItem *item() const;
+ void setHasCursor(bool hasCursor);
+
+ CursorNavigation *m_cursorNavigation;
+ bool m_acceptsCursor;
+ bool m_hasCursor;
+ bool m_trapsCursor;
+
+ friend class CursorNavigation;
+};
+
+#endif // CURSORNAVIGATIONATTACHED_H
diff --git a/plugin/inputadapter.cpp b/plugin/inputadapter.cpp
new file mode 100644
index 0000000..5814e5d
--- /dev/null
+++ b/plugin/inputadapter.cpp
@@ -0,0 +1,83 @@
+#include "inputadapter.h"
+#include "cursornavigation.h"
+#include <QEvent>
+#include <QKeyEvent>
+#include <QMouseEvent>
+#include <QWheelEvent>
+
+InputAdapter::InputAdapter(QObject *target, CursorNavigation *cursorNavigation)
+ : QObject(cursorNavigation)
+ ,m_target(target)
+ ,m_cursorNavigation(cursorNavigation)
+{
+ if (m_target)
+ m_target->installEventFilter(this);
+}
+
+bool InputAdapter::eventFilter(QObject *object, QEvent *event)
+{
+ if (object != m_target)
+ return false;
+ if (event->type() == QEvent::KeyPress) {
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
+ return handleKeyEvent(keyEvent);
+ } else if (event->type() == QEvent::Wheel) {
+ QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);
+ return handleWheelEvent(wheelEvent);
+ } else if (event->type() == QEvent::MouseMove) {
+ QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
+ return handleMouseEvent(mouseEvent);
+ }
+ return false;
+}
+
+bool InputAdapter::handleKeyEvent(QKeyEvent *event)
+{
+ CursorNavigationCommand cmd;
+ //detect arrow keys, tabs, enter and esc
+ switch (event->key())
+ {
+ case Qt::Key_Left:
+ cmd = CursorNavigationCommand::Left;
+ break;
+ case Qt::Key_Right:
+ cmd = CursorNavigationCommand::Right;
+ break;
+ case Qt::Key_Up:
+ cmd = CursorNavigationCommand::Up;
+ break;
+ case Qt::Key_Down:
+ cmd = CursorNavigationCommand::Down;
+ break;
+ case Qt::Key_Return:
+ case Qt::Key_Enter:
+ cmd.action = CursorNavigationCommand::Activate;
+ break;
+ case Qt::BackButton:
+ case Qt::Key_Escape:
+ cmd.action = CursorNavigationCommand::Escape;
+ break;
+ case Qt::Key_Tab:
+ cmd.action = CursorNavigationCommand::Escape;
+ break;
+ case Qt::Key_Backtab:
+ cmd.action = CursorNavigationCommand::Escape;
+ break;
+ default:
+ return false;
+ }
+
+ return m_cursorNavigation->inputCommand(cmd);
+}
+
+bool InputAdapter::handleMouseEvent(QMouseEvent *event)
+{
+ //interpret mouse movement as omnnidirectional joystick movements for testing purposes
+ return false;
+}
+
+bool InputAdapter::handleWheelEvent(QWheelEvent *event)
+{
+ //turn wheel events into tabs
+ return false;
+}
diff --git a/plugin/inputadapter.h b/plugin/inputadapter.h
new file mode 100644
index 0000000..0613751
--- /dev/null
+++ b/plugin/inputadapter.h
@@ -0,0 +1,40 @@
+#ifndef INPUTADAPTER_H
+#define INPUTADAPTER_H
+
+#include <QObject>
+#include "inputtypes.h"
+
+class CursorNavigation;
+class QQuickWindow;
+class QKeyEvent;
+class QMouseEvent;
+class QWheelEvent;
+
+/* filter various input events and translate them for the cursor navigation.
+ * it is possible to interpret mouse events as joystick/swipe events
+ * Set instance of this class as an input filter to the window or component that
+ * is being tracked.
+ * Events are passed forward to the CursorNavigation class, which should accept
+ * the event or reject it. When rejected, event is passed on.
+ */
+
+class InputAdapter : public QObject
+{
+ Q_OBJECT
+public:
+ InputAdapter(QObject *target, CursorNavigation *cursorNavigation);
+
+protected:
+ bool eventFilter(QObject *object, QEvent *event) override;
+
+private:
+ bool handleKeyEvent(QKeyEvent *ev);
+ bool handleMouseEvent(QMouseEvent *ev);
+ bool handleWheelEvent(QWheelEvent *ev);
+
+ QObject *const m_target;
+ CursorNavigation *m_cursorNavigation;
+
+};
+
+#endif // INPUTADAPTER_H
diff --git a/plugin/inputtypes.cpp b/plugin/inputtypes.cpp
new file mode 100644
index 0000000..770b03d
--- /dev/null
+++ b/plugin/inputtypes.cpp
@@ -0,0 +1,23 @@
+#include "inputtypes.h"
+
+const CursorNavigationCommand CursorNavigationCommand::Up(1.0, 270.0);
+const CursorNavigationCommand CursorNavigationCommand::Down(1.0, 90.0);
+const CursorNavigationCommand CursorNavigationCommand::Left(1.0, 180.0);
+const CursorNavigationCommand CursorNavigationCommand::Right(1.0, 0.0);
+
+CursorNavigationCommand::CursorNavigationCommand()
+ :magnitude(-1), angle(-1), action(NoAction)
+{}
+
+CursorNavigationCommand::CursorNavigationCommand(float magnitude, int angle)
+ :magnitude(magnitude), angle(angle), action(NoAction)
+{}
+
+//test if this commands angle is between given angles. clockwise from begin to end
+bool CursorNavigationCommand::angleIsBetween(int begin, int end) const
+{
+ if (begin > end)
+ return angle >= begin || angle <= end;
+ else
+ return angle >= begin && angle <= end;
+}
diff --git a/plugin/inputtypes.h b/plugin/inputtypes.h
new file mode 100644
index 0000000..bf7e82e
--- /dev/null
+++ b/plugin/inputtypes.h
@@ -0,0 +1,33 @@
+#ifndef INPUTTYPES_H
+#define INPUTTYPES_H
+
+struct CursorNavigationCommand
+{
+ enum Action
+ {
+ NoAction,
+ Forward, //tab
+ Back, //ctrl-tab
+ Activate, //enter/click on item
+ Escape //leave scope
+ };
+
+ CursorNavigationCommand();
+
+ CursorNavigationCommand(float magnitude, int angle);
+
+ //test if this commands angle is between given angles. clockwise from begin to end
+ bool angleIsBetween(int begin, int end) const;
+
+ float magnitude; //0.0 to 1.0
+ int angle; //0 to 359
+ Action action;
+
+ static const CursorNavigationCommand Up;
+ static const CursorNavigationCommand Down;
+ static const CursorNavigationCommand Left;
+ static const CursorNavigationCommand Right;
+
+};
+
+#endif // INPUTTYPES_H
diff --git a/plugin/itemregister.cpp b/plugin/itemregister.cpp
new file mode 100644
index 0000000..f4555f1
--- /dev/null
+++ b/plugin/itemregister.cpp
@@ -0,0 +1,36 @@
+#include "itemregister.h"
+#include <QQuickItem>
+
+ItemRegister::ItemRegister()
+{
+
+}
+
+void ItemRegister::registerItem(QQuickItem* item)
+{
+ if (!item)
+ return;
+
+ m_items.append(item);
+ connect(item, &QQuickItem::destroyed, this, &ItemRegister::onItemDestroyed);
+}
+
+void ItemRegister::unregisterItem(QQuickItem* item)
+{
+ if (!item)
+ return;
+
+ disconnect(item, &QQuickItem::destroyed, this, &ItemRegister::onItemDestroyed);
+ m_items.removeOne(item);
+}
+
+const QList<QQuickItem*> ItemRegister::items() const
+{
+ return m_items;
+}
+
+void ItemRegister::onItemDestroyed(QObject *obj)
+{
+ QQuickItem *item=static_cast<QQuickItem*>(obj);
+ m_items.removeOne(item);
+}
diff --git a/plugin/itemregister.h b/plugin/itemregister.h
new file mode 100644
index 0000000..479bca7
--- /dev/null
+++ b/plugin/itemregister.h
@@ -0,0 +1,30 @@
+#ifndef ITEMREGISTER_H
+#define ITEMREGISTER_H
+
+#include <QObject>
+
+class QQuickItem;
+
+//keeps track of items that are cursor navigable
+class ItemRegister : public QObject
+{
+ Q_OBJECT
+
+public:
+ ItemRegister();
+
+ void registerItem(QQuickItem* item);
+ void unregisterItem(QQuickItem* item);
+
+ const QList<QQuickItem*> items() const;
+
+private Q_SLOTS:
+ void onItemDestroyed(QObject *obj);
+
+private:
+ //for now the data structure is just a list. could be replaced with something more efficient for the final purpose
+ QList<QQuickItem*> m_items;
+
+};
+
+#endif // ITEMREGISTER_H
diff --git a/plugin/plugin.cpp b/plugin/plugin.cpp
new file mode 100644
index 0000000..26d156a
--- /dev/null
+++ b/plugin/plugin.cpp
@@ -0,0 +1,13 @@
+#include "plugin.h"
+#include "cursornavigation.h"
+#include "qqml.h"
+
+CursorNavigationPlugin::CursorNavigationPlugin()
+{
+}
+
+void CursorNavigationPlugin::registerTypes(const char *uri)
+{
+ qmlRegisterUncreatableType<CursorNavigation>(uri, 1, 0, "CursorNavigation",
+ QStringLiteral("CursorNavigation is not creatable, use the attached properties."));
+}
diff --git a/plugin/plugin.h b/plugin/plugin.h
new file mode 100644
index 0000000..237c7b2
--- /dev/null
+++ b/plugin/plugin.h
@@ -0,0 +1,16 @@
+#ifndef PLUGIN_H
+#define PLUGIN_H
+
+#include <QQmlExtensionPlugin>
+
+class CursorNavigationPlugin : public QQmlExtensionPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
+
+public:
+ CursorNavigationPlugin();
+ void registerTypes(const char *uri);
+};
+
+#endif // PLUGIN_H
diff --git a/plugin/plugin.pro b/plugin/plugin.pro
new file mode 100644
index 0000000..72c6b96
--- /dev/null
+++ b/plugin/plugin.pro
@@ -0,0 +1,36 @@
+TARGET = cursornavigationplugin
+TEMPLATE = lib
+
+QT -= gui
+QT += qml quick
+
+CONFIG += plugin
+
+DEFINES += PLUGIN_LIBRARY
+
+SOURCES += \
+ plugin.cpp \
+ cursornavigation.cpp \
+ cursornavigationattached.cpp \
+ itemregister.cpp \
+ inputadapter.cpp \
+ cursornavigationalgorithm.cpp \
+ spatialnavigation4dir.cpp \
+ inputtypes.cpp
+
+HEADERS += \
+ plugin.h \
+ cursornavigation.h \
+ cursornavigationattached.h \
+ itemregister.h \
+ inputadapter.h \
+ inputtypes.h \
+ cursornavigationalgorithm.h \
+ spatialnavigation4dir.h
+
+pluginfiles.files += qmldir
+
+target.path = $$[QT_INSTALL_QML]/CursorNavigation
+pluginfiles.path = $$[QT_INSTALL_QML]/CursorNavigation
+
+INSTALLS += target pluginfiles
diff --git a/plugin/qmldir b/plugin/qmldir
new file mode 100644
index 0000000..02cff60
--- /dev/null
+++ b/plugin/qmldir
@@ -0,0 +1,2 @@
+module CursorNavigation
+plugin cursornavigationplugin
diff --git a/plugin/spatialnavigation4dir.cpp b/plugin/spatialnavigation4dir.cpp
new file mode 100644
index 0000000..fa1657e
--- /dev/null
+++ b/plugin/spatialnavigation4dir.cpp
@@ -0,0 +1,161 @@
+#include "spatialnavigation4dir.h"
+#include <QQuickItem>
+#include <QDebug>
+#include <algorithm>
+#include <functional>
+
+//we only compare distances to eachother so no need to calculate expensive
+//square roots. centerpoint comparison is just enough for now too
+float distanceSquared(const QRectF& item1, const QRectF& item2)
+{
+ QPointF p1=item1.center();
+ QPointF p2=item2.center();
+ float dx=p1.x()-p2.x();
+ float dy=p1.y()-p2.y();
+ return dx*dx+dy*dy;
+}
+
+SpatialNavigation4Dir::SpatialNavigation4Dir(ItemRegister *itemRegister)
+ :CursorNavigationAlgorithm (itemRegister)
+{
+
+}
+
+QQuickItem* SpatialNavigation4Dir::getNextCandidate(const QList<QQuickItem*> &candidates,
+ const QQuickItem *currentItem,
+ const CursorNavigationCommand &cmd)
+{
+ if (candidates.isEmpty())
+ return nullptr;
+
+ qDebug() << "spatial chooser called, no of candidates=" << candidates.count();
+
+ if (!currentItem && candidates.size()) {
+ qDebug() << "the spatial chooser falling back to first child" << candidates.first();
+ return candidates.first();
+ }
+
+ //picking the next item according to the current items location and the command:
+ //-check direction
+ //-choose candidates in that general direction currentItem our current item (up, down, left or right)
+ //-currentItem those pick ones inside of current items projection
+ // -currentItem those within the projection pick the closest one
+ //-if no hits within the projection, then take the closest with distance just in the general direction
+ //this algorithm uses the scene coordinates of the items
+
+ std::function<bool(const QRectF&)> isInDirection;
+ std::function<bool(const QRectF&)> isInProjection;
+
+ //scene coords of the current item
+ const QRectF currentItemSceneRect = currentItem->mapRectToScene(QRectF( 0, 0,
+ currentItem->width(), currentItem->height() ));
+
+ //NOTICE: overlapping candidates will be ignored for now (TODO, this needs to be changed)
+
+ if (cmd.angleIsBetween(315, 45) || cmd.angleIsBetween(135, 225) ) {
+ //if (cmd == CursorNavigationCommand::Right || cmd == CursorNavigationCommand::Left) {
+
+ isInProjection = [&currentItemSceneRect](const QRectF &itemRect) {
+ return !( currentItemSceneRect.y() > itemRect.y()+itemRect.height() ||
+ currentItemSceneRect.y()+currentItemSceneRect.height() < itemRect.y() );
+ };
+ if (cmd.angleIsBetween(315, 45)) {
+ //if (cmd == Command_Right) {
+ isInDirection = [&currentItemSceneRect](const QRectF &itemRect) {
+ return currentItemSceneRect.x()+currentItemSceneRect.width() <= itemRect.x();
+ };
+ } else {
+ isInDirection = [&currentItemSceneRect](const QRectF &itemRect) {
+ return currentItemSceneRect.x() >= itemRect.x()+itemRect.width();
+ };
+ }
+
+ } else if (cmd.angleIsBetween(225, 315) || cmd.angleIsBetween(45, 135)) {
+ //} else if (cmd == Command_Up || cmd == Command_Down) {
+ isInProjection = [&currentItemSceneRect](const QRectF &itemRect) {
+ return !( currentItemSceneRect.x() > itemRect.x()+itemRect.width() ||
+ currentItemSceneRect.x()+currentItemSceneRect.width() < itemRect.x() );
+ };
+ if (cmd.angleIsBetween(45, 135)) {
+ //if (cmd == Command_Down) {
+ isInDirection = [&currentItemSceneRect](const QRectF &itemRect) {
+ return currentItemSceneRect.y()+currentItemSceneRect.height() <= itemRect.y();
+ };
+ } else {
+ isInDirection = [currentItemSceneRect](const QRectF &itemRect) {
+ return currentItemSceneRect.y() >= itemRect.y()+itemRect.height();
+ };
+ }
+ } else {
+ return nullptr;
+ }
+
+ std::pair<QQuickItem*,int> closest(nullptr,0);
+
+ //qDebug() << "current: x=" << currentItemSceneRect.x() << " y=" << currentItemSceneRect.y();
+
+ for (auto candidate : candidates)
+ {
+ if (!candidate->isVisible() || !candidate->isEnabled()) {
+ //qDebug() << "skipping a invisible/disabled item";
+ continue;
+ }
+
+ //scene coords of the candidate
+ QRectF candidateSceneRect = candidate->mapRectToScene(
+ QRect( 0, 0,
+ candidate->width(), candidate->height() ));
+
+ //qDebug() << "x=" << candidateSceneRect.x() << " y=" << candidateSceneRect.y();
+
+ if (isInDirection(candidateSceneRect) && isInProjection(candidateSceneRect))
+ {
+ //qDebug() << " is in direction and projection...";
+ int dist = distanceSquared(currentItemSceneRect,candidateSceneRect);
+ if (closest.second > dist || !closest.first)
+ {
+ closest.second = dist;
+ closest.first = candidate;
+ }
+ }
+ }
+
+ if (closest.first)
+ {
+ qDebug() << "chosen one: " << closest.first->mapRectToScene(
+ QRect( 0, 0,
+ closest.first->width(), closest.first->height() ));
+ }
+
+ if (!closest.first) {
+ //qDebug() << Q_FUNC_INFO << " looking for a candidate in the general direction...";
+
+ for (auto candidate : candidates)
+ {
+ if (!candidate->isVisible() || !candidate->isEnabled()) {
+ //qDebug() << "skipping a invisible/disabled item";
+ continue;
+ }
+
+ //scene coords of the candidate
+ QRectF candidateSceneRect = candidate->mapRectToScene(
+ QRect( 0, 0,
+ candidate->width(), candidate->height() ));
+
+ if (isInDirection(candidateSceneRect))
+ {
+ int dist = distanceSquared(currentItemSceneRect,candidateSceneRect);
+ if (closest.second > dist || !closest.first)
+ {
+ closest.second = dist;
+ closest.first = candidate;
+ }
+ }
+ }
+ }
+
+ qDebug() << Q_FUNC_INFO << " selected candidate:" << closest.first;
+
+ return closest.first;
+
+}
diff --git a/plugin/spatialnavigation4dir.h b/plugin/spatialnavigation4dir.h
new file mode 100644
index 0000000..a91c505
--- /dev/null
+++ b/plugin/spatialnavigation4dir.h
@@ -0,0 +1,17 @@
+#ifndef SPATIALNAVIGATION_H
+#define SPATIALNAVIGATION_H
+
+#include "cursornavigationalgorithm.h"
+
+class SpatialNavigation4Dir : public CursorNavigationAlgorithm
+{
+public:
+ SpatialNavigation4Dir(ItemRegister *itemRegister);
+
+ virtual QQuickItem* getNextCandidate(const QList<QQuickItem*> &candidates,
+ const QQuickItem *currentItem,
+ const CursorNavigationCommand &cmd);
+
+};
+
+#endif // SPATIALNAVIGATION_H
diff --git a/tests/tests.pro b/tests/tests.pro
new file mode 100644
index 0000000..2e9bc7e
--- /dev/null
+++ b/tests/tests.pro
@@ -0,0 +1,10 @@
+QT += testlib
+QT -= gui
+
+CONFIG += qt console warn_on depend_includepath testcase
+CONFIG -= app_bundle
+
+TEMPLATE = app
+
+SOURCES += \
+ tst_cursornavigation.cpp
diff --git a/tests/tst_cursornavigation.cpp b/tests/tst_cursornavigation.cpp
new file mode 100644
index 0000000..d61f635
--- /dev/null
+++ b/tests/tst_cursornavigation.cpp
@@ -0,0 +1,58 @@
+#include <QtTest>
+
+// add necessary includes here
+
+class TestCursorNavigation : public QObject
+{
+ Q_OBJECT
+
+public:
+ TestCursorNavigation();
+ ~TestCursorNavigation();
+
+private slots:
+ void test_pluginLoading();
+ void test_registering();
+ void test_withKeyNavigation();
+ void test_spatial4Directions();
+
+};
+
+TestCursorNavigation::TestCursorNavigation()
+{
+
+}
+
+TestCursorNavigation::~TestCursorNavigation()
+{
+
+}
+
+void TestCursorNavigation::test_pluginLoading()
+{
+ //test that the plugin loads and is available for use and a element with an attached property can be set
+}
+
+void TestCursorNavigation::test_registering()
+{
+ //see that elements marked for accepting cursor are added to the engines element register
+ //adding the property should add item to the register
+ //deleting the item should unregister the item
+ //unsetting the property should unregister the item
+}
+
+void TestCursorNavigation::test_withKeyNavigation()
+{
+ //test that element that additionally uses KeyNavigation, behaves primarily according to the KeyNavigation
+ //ie Cursor navigation plugin should not override KeyNavigation
+ //arrows + tab/backtab
+}
+
+void TestCursorNavigation::test_spatial4Directions()
+{
+ //test the spatial algorithm in the basic 4 directional case
+}
+
+QTEST_APPLESS_MAIN(TestCursorNavigation)
+
+#include "tst_cursornavigation.moc"