Merge branch 'improve_navigation_cpp' into feature/android-tv

This commit is contained in:
albexk 2024-11-25 12:20:31 +03:00
commit c72d76aec7
88 changed files with 3939 additions and 3369 deletions

View file

@ -0,0 +1,324 @@
#include "focusController.h"
#include "listViewFocusController.h"
#include <QQuickWindow>
#include <QQmlApplicationEngine>
bool isListView(QObject* item)
{
return item->inherits("QQuickListView");
}
bool isOnTheScene(QObject* object)
{
QQuickItem* item = qobject_cast<QQuickItem*>(object);
if (!item) {
qWarning() << "Couldn't recognize object as item";
return false;
}
if (!item->isVisible()) {
// qDebug() << "===>> The item is not visible: " << item;
return false;
}
QRectF itemRect = item->mapRectToScene(item->childrenRect());
QQuickWindow* window = item->window();
if (!window) {
qWarning() << "Couldn't get the window on the Scene check";
return false;
}
const auto contentItem = window->contentItem();
if (!contentItem) {
qWarning() << "Couldn't get the content item on the Scene check";
return false;
}
QRectF windowRect = contentItem->childrenRect();
const auto res = (windowRect.contains(itemRect) || isListView(item));
// qDebug() << (res ? "===>> item is inside the Scene" : "===>> ITEM IS OUTSIDE THE SCENE") << " itemRect: " << itemRect << "; windowRect: " << windowRect;
return res;
}
QList<QObject*> getSubChain(QObject* object)
{
QList<QObject*> res;
if (!object) {
qDebug() << "The object is NULL";
return res;
}
const auto children = object->children();
for(const auto child : children) {
if (child
&& isFocusable(child)
&& isOnTheScene(child)
&& isEnabled(child)
) {
res.append(child);
} else {
res.append(getSubChain(child));
}
}
return res;
}
FocusController::FocusController(QQmlApplicationEngine* engine, QObject *parent)
: QObject{parent}
, m_engine{engine}
, m_focusChain{}
, m_focusedItem{nullptr}
, m_rootObjects{}
, m_defaultFocusItem{QSharedPointer<QQuickItem>()}
, m_lvfc{nullptr}
{
QObject::connect(m_engine.get(), &QQmlApplicationEngine::objectCreated, this, [this](QObject *object, const QUrl &url){
QQuickItem* newDefaultFocusItem = object->findChild<QQuickItem*>("defaultFocusItem");
if(newDefaultFocusItem && m_defaultFocusItem != newDefaultFocusItem) {
m_defaultFocusItem.reset(newDefaultFocusItem);
qDebug() << "===>> NEW DEFAULT FOCUS ITEM: " << m_defaultFocusItem;
}
});
QObject::connect(this, &FocusController::focusedItemChanged, this, [this]() {
m_focusedItem->forceActiveFocus(Qt::TabFocusReason);
});
}
void FocusController::nextKeyTabItem()
{
nextItem(Direction::Forward);
}
void FocusController::previousKeyTabItem()
{
nextItem(Direction::Backward);
}
void FocusController::nextKeyUpItem()
{
nextItem(Direction::Backward);
}
void FocusController::nextKeyDownItem()
{
nextItem(Direction::Forward);
}
void FocusController::nextKeyLeftItem()
{
nextItem(Direction::Backward);
}
void FocusController::nextKeyRightItem()
{
nextItem(Direction::Forward);
}
void FocusController::setFocusItem(QQuickItem* item)
{
if (m_focusedItem != item) {
m_focusedItem = item;
emit focusedItemChanged();
qDebug() << "===>> FocusItem is changed to " << item << "!";
} else {
qDebug() << "===>> FocusItem is is the same: " << item << "!";
}
}
void FocusController::setFocusOnDefaultItem()
{
qDebug() << "===>> Setting focus on DEFAULT FOCUS ITEM...";
setFocusItem(m_defaultFocusItem.get());
}
void FocusController::pushRootObject(QObject* object)
{
qDebug() << "===>> Calling < pushRootObject >...";
m_rootObjects.push(object);
dropListView();
// setFocusOnDefaultItem();
qDebug() << "===>> ROOT OBJECT is changed to: " << m_rootObjects.top();
qDebug() << "===>> ROOT OBJECTS: " << m_rootObjects;
}
void FocusController::dropRootObject(QObject* object)
{
qDebug() << "===>> Calling < dropRootObject >...";
if (m_rootObjects.empty()) {
qDebug() << "ROOT OBJECT is already DEFAULT";
return;
}
if (m_rootObjects.top() == object) {
m_rootObjects.pop();
dropListView();
setFocusOnDefaultItem();
if(m_rootObjects.size()) {
qDebug() << "===>> ROOT OBJECT is changed to: " << m_rootObjects.top();
} else {
qDebug() << "===>> ROOT OBJECT is changed to DEFAULT";
}
} else {
qWarning() << "===>> TRY TO DROP WRONG ROOT OBJECT: " << m_rootObjects.top() << " SHOULD BE: " << object;
}
}
void FocusController::resetRootObject()
{
qDebug() << "===>> Calling < resetRootObject >...";
m_rootObjects.clear();
qDebug() << "===>> ROOT OBJECT IS RESETED";
}
void FocusController::reload(Direction direction)
{
qDebug() << "===>> Calling < reload >...";
m_focusChain.clear();
QObject* rootObject = (m_rootObjects.empty()
? m_engine->rootObjects().value(0)
: m_rootObjects.top());
if(!rootObject) {
qCritical() << "No ROOT OBJECT found!";
resetRootObject();
dropListView();
return;
}
qDebug() << "===>> ROOT OBJECTS: " << rootObject;
m_focusChain.append(getSubChain(rootObject));
std::sort(m_focusChain.begin(), m_focusChain.end(), direction == Direction::Forward ? isLess : isMore);
if (m_focusChain.empty()) {
qWarning() << "Focus chain is empty!";
resetRootObject();
dropListView();
return;
}
}
void FocusController::nextItem(Direction direction)
{
qDebug() << "===>> Calling < nextItem >...";
reload(direction);
if (m_lvfc && isListView(m_focusedItem)) {
direction == Direction::Forward ? focusNextListViewItem() : focusPreviousListViewItem();
qDebug() << "===>> Handling the [ ListView ]...";
return;
}
if(m_focusChain.empty()) {
qWarning() << "There are no items to navigate";
setFocusOnDefaultItem();
return;
}
auto focusedItemIndex = m_focusChain.indexOf(m_focusedItem);
if (focusedItemIndex == -1) {
qDebug() << "Current FocusItem is not in chain, switch to first in chain...";
focusedItemIndex = 0;
} else if (focusedItemIndex == (m_focusChain.size() - 1)) {
qDebug() << "Last focus index. Starting from the beginning...";
focusedItemIndex = 0;
} else {
qDebug() << "Incrementing focus index...";
focusedItemIndex++;
}
const auto focusedItem = qobject_cast<QQuickItem*>(m_focusChain.at(focusedItemIndex));
if(focusedItem == nullptr) {
qWarning() << "Failed to get item to focus on. Setting focus on default";
setFocusOnDefaultItem();
return;
}
if(isListView(focusedItem)) {
qDebug() << "===>> Found [ListView]";
m_lvfc = new ListViewFocusController(focusedItem, this);
m_focusedItem = focusedItem;
if(direction == Direction::Forward) {
m_lvfc->nextDelegate();
focusNextListViewItem();
} else {
m_lvfc->previousDelegate();
focusPreviousListViewItem();
}
return;
}
setFocusItem(focusedItem);
printItems(m_focusChain, focusedItem);
///////////////////////////////////////////////////////////
const auto w = m_defaultFocusItem->window();
qDebug() << "===>> CURRENT ACTIVE ITEM: " << w->activeFocusItem();
qDebug() << "===>> CURRENT FOCUS OBJECT: " << w->focusObject();
if(m_rootObjects.empty()) {
qDebug() << "===>> ROOT OBJECT IS DEFAULT";
} else {
qDebug() << "===>> ROOT OBJECT: " << m_rootObjects.top();
}
}
void FocusController::focusNextListViewItem()
{
qDebug() << "===>> Calling < focusNextListViewItem >...";
if (m_lvfc->isLastFocusItemInListView() || m_lvfc->isReturnNeeded()) {
qDebug() << "===>> Last item in [ ListView ] was reached. Going to the NEXT element after [ ListView ]";
dropListView();
nextItem(Direction::Forward);
return;
} else if (m_lvfc->isLastFocusItemInDelegate()) {
qDebug() << "===>> End of delegate elements was reached. Going to the next delegate";
m_lvfc->resetFocusChain();
m_lvfc->nextDelegate();
}
m_lvfc->focusNextItem();
}
void FocusController::focusPreviousListViewItem()
{
qDebug() << "===>> Calling < focusPreviousListViewItem >...";
if (m_lvfc->isFirstFocusItemInListView() || m_lvfc->isReturnNeeded()) {
qDebug() << "===>> First item in [ ListView ] was reached. Going to the PREVIOUS element after [ ListView ]";
dropListView();
nextItem(Direction::Backward);
return;
} else if (m_lvfc->isFirstFocusItemInDelegate()) {
m_lvfc->resetFocusChain();
m_lvfc->previousDelegate();
}
m_lvfc->focusPreviousItem();
}
void FocusController::dropListView()
{
qDebug() << "===>> Calling < dropListView >...";
if(m_lvfc) {
delete m_lvfc;
m_lvfc = nullptr;
}
}

View file

@ -0,0 +1,62 @@
#ifndef FOCUSCONTROLLER_H
#define FOCUSCONTROLLER_H
#include <QObject>
#include <QStack>
#include <QSharedPointer>
class QQuickItem;
class QQmlApplicationEngine;
class ListViewFocusController;
/*!
* \brief The FocusController class makes focus control more straightforward
* \details Focus is handled only for visible and enabled items which have
* `isFocused` property from top left to bottom right.
* \note There are items handled differently (e.g. ListView)
*/
class FocusController : public QObject
{
Q_OBJECT
public:
explicit FocusController(QQmlApplicationEngine* engine, QObject *parent = nullptr);
~FocusController() override = default;
Q_INVOKABLE void nextKeyTabItem();
Q_INVOKABLE void previousKeyTabItem();
Q_INVOKABLE void nextKeyUpItem();
Q_INVOKABLE void nextKeyDownItem();
Q_INVOKABLE void nextKeyLeftItem();
Q_INVOKABLE void nextKeyRightItem();
Q_INVOKABLE void setFocusItem(QQuickItem* item);
Q_INVOKABLE void setFocusOnDefaultItem();
Q_INVOKABLE void pushRootObject(QObject* object);
Q_INVOKABLE void dropRootObject(QObject* object);
Q_INVOKABLE void resetRootObject();
private:
enum class Direction {
Forward,
Backward,
};
void reload(Direction direction);
void nextItem(Direction direction);
void focusNextListViewItem();
void focusPreviousListViewItem();
void dropListView();
QSharedPointer<QQmlApplicationEngine> m_engine; // Pointer to engine to get root object
QList<QObject*> m_focusChain; // List of current objects to be focused
QQuickItem* m_focusedItem; // Pointer to the active focus item
QStack<QObject*> m_rootObjects;
QSharedPointer<QQuickItem> m_defaultFocusItem;
ListViewFocusController* m_lvfc; // ListView focus manager
signals:
void focusedItemChanged();
};
#endif // FOCUSCONTROLLER_H

View file

@ -0,0 +1,393 @@
#include "listViewFocusController.h"
#include <QQuickItem>
#include <QQueue>
#include <QPointF>
#include <QRectF>
#include <QQuickWindow>
bool isVisible(QObject* item)
{
const auto res = item->property("visible").toBool();
return res;
}
bool isFocusable(QObject* item)
{
const auto res = item->property("isFocusable").toBool();
return res;
}
QPointF getItemCenterPointOnScene(QQuickItem* item)
{
const auto x0 = item->x() + (item->width() / 2);
const auto y0 = item->y() + (item->height() / 2);
return item->parentItem()->mapToScene(QPointF{x0, y0});
}
bool isLess(QObject* item1, QObject* item2)
{
const auto p1 = getItemCenterPointOnScene(qobject_cast<QQuickItem*>(item1));
const auto p2 = getItemCenterPointOnScene(qobject_cast<QQuickItem*>(item2));
return (p1.y() == p2.y()) ? (p1.x() < p2.x()) : (p1.y() < p2.y());
}
bool isMore(QObject* item1, QObject* item2)
{
return !isLess(item1, item2);
}
bool isEnabled(QObject* obj)
{
const auto item = qobject_cast<QQuickItem*>(obj);
return item && item->isEnabled();
}
QList<QObject*> getItemsChain(QObject* object)
{
QList<QObject*> res;
if (!object) {
qDebug() << "The object is NULL";
return res;
}
const auto children = object->children();
for(const auto child : children) {
if (child
&& isFocusable(child)
&& isEnabled(child)
&& isVisible(child)
) {
res.append(child);
} else {
res.append(getItemsChain(child));
}
}
return res;
}
void printItems(const QList<QObject*>& items, QObject* current_item)
{
for(const auto& item : items) {
QQuickItem* i = qobject_cast<QQuickItem*>(item);
QPointF coords {getItemCenterPointOnScene(i)};
QString prefix = current_item == i ? "==>" : " ";
qDebug() << prefix << " Item: " << i << " with coords: " << coords;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
ListViewFocusController::ListViewFocusController(QQuickItem* listView, QObject* parent)
: QObject{parent}
, m_listView{listView}
, m_focusChain{}
, m_currentSection{Section::Default}
, m_header{nullptr}
, m_footer{nullptr}
, m_focusedItem{nullptr}
, m_focusedItemIndex{-1}
, m_delegateIndex{0}
, m_isReturnNeeded{false}
, m_currentSectionString {"Default", "Header", "Delegate", "Footer"}
{
QVariant headerItemProperty = m_listView->property("headerItem");
m_header = headerItemProperty.canConvert<QQuickItem*>() ? headerItemProperty.value<QQuickItem*>() : nullptr;
QVariant footerItemProperty = m_listView->property("footerItem");
m_footer = footerItemProperty.canConvert<QQuickItem*>() ? footerItemProperty.value<QQuickItem*>() : nullptr;
}
ListViewFocusController::~ListViewFocusController()
{
}
void ListViewFocusController::viewAtCurrentIndex() const
{
switch(m_currentSection) {
case Section::Default:
[[fallthrough]];
case Section::Header: {
qDebug() << "===>> [FOCUS ON BEGINNING...]";
QMetaObject::invokeMethod(m_listView, "positionViewAtBeginning");
break;
}
case Section::Delegate: {
qDebug() << "===>> [FOCUS ON INDEX...]";
QMetaObject::invokeMethod(m_listView, "positionViewAtIndex",
Q_ARG(int, m_delegateIndex), // Index
Q_ARG(int, 2)); // PositionMode (0 = Visible)
break;
}
case Section::Footer: {
qDebug() << "===>> [FOCUS ON END...]";
QMetaObject::invokeMethod(m_listView, "positionViewAtEnd");
break;
}
}
}
int ListViewFocusController::size() const
{
return m_listView->property("count").toInt();
}
int ListViewFocusController::currentIndex() const
{
return m_delegateIndex;
}
void ListViewFocusController::nextDelegate()
{
const auto sectionName = m_currentSectionString[static_cast<int>(m_currentSection)];
qDebug() << "===>> [nextDelegate... current section: " << sectionName << " ]";
switch(m_currentSection) {
case Section::Default: {
if(hasHeader()) {
m_currentSection = Section::Header;
viewAtCurrentIndex();
break;
}
[[fallthrough]];
}
case Section::Header: {
if (size() > 0) {
m_currentSection = Section::Delegate;
viewAtCurrentIndex();
break;
}
[[fallthrough]];
}
case Section::Delegate:
if (m_delegateIndex < (size() - 1)) {
m_delegateIndex++;
viewAtCurrentIndex();
break;
} else if (hasFooter()) {
m_currentSection = Section::Footer;
viewAtCurrentIndex();
break;
}
[[fallthrough]];
case Section::Footer: {
m_isReturnNeeded = true;
m_currentSection = Section::Default;
viewAtCurrentIndex();
break;
}
default: {
qCritical() << "Current section is invalid!";
break;
}
}
}
void ListViewFocusController::previousDelegate()
{
switch(m_currentSection) {
case Section::Default: {
if(hasFooter()) {
m_currentSection = Section::Footer;
break;
}
[[fallthrough]];
}
case Section::Footer: {
if (size() > 0) {
m_currentSection = Section::Delegate;
m_delegateIndex = size() - 1;
break;
}
[[fallthrough]];
}
case Section::Delegate: {
if (m_delegateIndex > 0) {
m_delegateIndex--;
break;
} else if (hasHeader()) {
m_currentSection = Section::Header;
break;
}
[[fallthrough]];
}
case Section::Header: {
m_isReturnNeeded = true;
m_currentSection = Section::Default;
break;
}
default: {
qCritical() << "Current section is invalid!";
break;
}
}
}
void ListViewFocusController::decrementIndex()
{
m_delegateIndex--;
}
QQuickItem* ListViewFocusController::itemAtIndex(const int index) const
{
QQuickItem* item{nullptr};
QMetaObject::invokeMethod(m_listView, "itemAtIndex",
Q_RETURN_ARG(QQuickItem*, item),
Q_ARG(int, index));
return item;
}
QQuickItem* ListViewFocusController::currentDelegate() const
{
QQuickItem* result{nullptr};
switch(m_currentSection) {
case Section::Default: {
qWarning() << "No elements...";
break;
}
case Section::Header: {
result = m_header;
break;
}
case Section::Delegate: {
result = itemAtIndex(m_delegateIndex);
break;
}
case Section::Footer: {
result = m_footer;
break;
}
}
return result;
}
QQuickItem* ListViewFocusController::focusedItem() const
{
return m_focusedItem;
}
void ListViewFocusController::focusNextItem()
{
if (m_isReturnNeeded) {
qDebug() << "===>> [ RETURN IS NEEDED... ]";
return;
}
m_focusChain = getItemsChain(currentDelegate());
if (m_focusChain.empty()) {
qWarning() << "No elements found in the delegate. Going to next delegate...";
nextDelegate();
focusNextItem();
return;
}
m_focusedItemIndex++;
m_focusedItem = qobject_cast<QQuickItem*>(m_focusChain.at(m_focusedItemIndex));
qDebug() << "==>> [ Focused Item: " << m_focusedItem << " with Index: " << m_focusedItemIndex << " ]";
m_focusedItem->forceActiveFocus(Qt::TabFocusReason);
}
void ListViewFocusController::focusPreviousItem()
{
if (m_isReturnNeeded) {
return;
}
if (m_focusChain.empty()) {
qDebug() << "Empty focusChain with current delegate: " << currentDelegate() << "Scanning for elements...";
m_focusChain = getItemsChain(currentDelegate());
}
if (m_focusChain.empty()) {
qWarning() << "No elements found in the delegate. Going to next delegate...";
previousDelegate();
focusPreviousItem();
return;
}
if (m_focusedItemIndex == -1) {
m_focusedItemIndex = m_focusChain.size();
}
m_focusedItemIndex--;
m_focusedItem = qobject_cast<QQuickItem*>(m_focusChain.at(m_focusedItemIndex));
qDebug() << "==>> [ Focused Item: " << m_focusedItem << " with Index: " << m_focusedItemIndex << " ]";
m_focusedItem->forceActiveFocus(Qt::TabFocusReason);
}
void ListViewFocusController::resetFocusChain()
{
m_focusChain.clear();
m_focusedItem = nullptr;
m_focusedItemIndex = -1;
}
bool ListViewFocusController::isFirstFocusItemInDelegate() const
{
return m_focusedItem && (m_focusedItem == m_focusChain.first());
}
bool ListViewFocusController::isLastFocusItemInDelegate() const
{
return m_focusedItem && (m_focusedItem == m_focusChain.last());
}
bool ListViewFocusController::hasHeader() const
{
return m_header && !getItemsChain(m_header).isEmpty();
}
bool ListViewFocusController::hasFooter() const
{
return m_footer && !getItemsChain(m_footer).isEmpty();
}
bool ListViewFocusController::isFirstFocusItemInListView() const
{
switch (m_currentSection) {
case Section::Footer: {
return isFirstFocusItemInDelegate() && !hasHeader() && (size() == 0);
}
case Section::Delegate: {
return isFirstFocusItemInDelegate() && (m_delegateIndex == 0) && !hasHeader();
}
case Section::Header: {
isFirstFocusItemInDelegate();
}
case Section::Default: {
return true;
}
default:
qWarning() << "Wrong section";
return true;
}
}
bool ListViewFocusController::isLastFocusItemInListView() const
{
switch (m_currentSection) {
case Section::Default: {
return !hasHeader() && (size() == 0) && !hasFooter();
}
case Section::Header: {
return isLastFocusItemInDelegate() && (size() == 0) && !hasFooter();
}
case Section::Delegate: {
return isLastFocusItemInDelegate() && (m_delegateIndex == size() - 1) && !hasFooter();
}
case Section::Footer: {
return isLastFocusItemInDelegate();
}
default:
qWarning() << "Wrong section";
return true;
}
}
bool ListViewFocusController::isReturnNeeded() const
{
return m_isReturnNeeded;
}

View file

@ -0,0 +1,76 @@
#ifndef LISTVIEWFOCUSCONTROLLER_H
#define LISTVIEWFOCUSCONTROLLER_H
#include <QObject>
#include <QStack>
#include <QSharedPointer>
#include <QQuickItem>
bool isEnabled(QObject* item);
bool isFocusable(QObject* item);
bool isMore(QObject* item1, QObject* item2);
bool isLess(QObject* item1, QObject* item2);
QList<QObject*> getSubChain(QObject* object);
void printItems(const QList<QObject*>& items, QObject* current_item);
/*!
* \brief The ListViewFocusController class manages the focus of elements in ListView
* \details This class object moving focus to ListView's controls since ListView stores
* it's data implicitly and it could be got one by one.
*
* This class was made to store as less as possible data getting it from QML
* when it's needed.
*/
class ListViewFocusController : public QObject
{
Q_OBJECT
public:
explicit ListViewFocusController(QQuickItem* listView, QObject* parent = nullptr);
~ListViewFocusController();
void nextDelegate();
void previousDelegate();
void decrementIndex();
void focusNextItem();
void focusPreviousItem();
void resetFocusChain();
bool isFirstFocusItemInListView() const;
bool isFirstFocusItemInDelegate() const;
bool isLastFocusItemInListView() const;
bool isLastFocusItemInDelegate() const;
bool isReturnNeeded() const;
private:
enum class Section {
Default,
Header,
Delegate,
Footer,
};
int size() const;
int currentIndex() const;
void viewAtCurrentIndex() const;
QQuickItem* itemAtIndex(const int index) const;
QQuickItem* currentDelegate() const;
QQuickItem* focusedItem() const;
bool hasHeader() const;
bool hasFooter() const;
QQuickItem* m_listView;
QList<QObject*> m_focusChain;
Section m_currentSection;
QQuickItem* m_header;
QQuickItem* m_footer;
QQuickItem* m_focusedItem; // Pointer to focused item on Delegate
qsizetype m_focusedItemIndex;
qsizetype m_delegateIndex;
bool m_isReturnNeeded;
QList<QString> m_currentSectionString;
};
#endif // LISTVIEWFOCUSCONTROLLER_H

View file

@ -81,7 +81,7 @@ void PageController::keyPressEvent(Qt::Key key)
case Qt::Key_Escape: {
if (m_drawerDepth) {
emit closeTopDrawer();
setDrawerDepth(getDrawerDepth() - 1);
decrementDrawerDepth();
} else {
emit escapePressed();
}
@ -142,11 +142,25 @@ void PageController::setDrawerDepth(const int depth)
}
}
int PageController::getDrawerDepth()
int PageController::getDrawerDepth() const
{
return m_drawerDepth;
}
int PageController::incrementDrawerDepth()
{
return ++m_drawerDepth;
}
int PageController::decrementDrawerDepth()
{
if (m_drawerDepth == 0) {
return m_drawerDepth;
} else {
return --m_drawerDepth;
}
}
void PageController::onShowErrorMessage(ErrorCode errorCode)
{
const auto fullErrorMessage = errorString(errorCode);

View file

@ -100,7 +100,9 @@ public slots:
void closeApplication();
void setDrawerDepth(const int depth);
int getDrawerDepth();
int getDrawerDepth() const;
int incrementDrawerDepth();
int decrementDrawerDepth();
private slots:
void onShowErrorMessage(amnezia::ErrorCode errorCode);
@ -135,9 +137,6 @@ signals:
void escapePressed();
void closeTopDrawer();
void forceTabBarActiveFocus();
void forceStackActiveFocus();
private:
QSharedPointer<ServersModel> m_serversModel;