mirror of
https://github.com/mudita/MuditaOS.git
synced 2026-01-02 19:08:54 -05:00
This is reimplementation ListViewWithLabels which is able to set correct page with focused item.
553 lines
16 KiB
C++
553 lines
16 KiB
C++
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
|
|
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
|
|
|
|
#include "ListViewEngine.hpp"
|
|
|
|
#include <cassert>
|
|
#include <log/log.hpp>
|
|
|
|
namespace gui
|
|
{
|
|
ListViewEngine::ListViewEngine(std::shared_ptr<ListItemProvider> prov)
|
|
{
|
|
setProvider(std::move(prov));
|
|
}
|
|
|
|
ListViewEngine::~ListViewEngine()
|
|
{
|
|
clear();
|
|
}
|
|
|
|
void ListViewEngine::setElementsCount(unsigned count)
|
|
{
|
|
if ((elementsCount != count) || (elementsCount == listview::nPos)) {
|
|
onElementsCountChanged(count);
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::setBoundaries(Boundaries value)
|
|
{
|
|
boundaries = value;
|
|
}
|
|
|
|
void ListViewEngine::setOrientation(listview::Orientation value)
|
|
{
|
|
orientation = value;
|
|
|
|
if (orientation == listview::Orientation::TopBottom) {
|
|
body->setAlignment(Alignment::Vertical::Top);
|
|
}
|
|
else {
|
|
body->setAlignment(Alignment::Vertical::Bottom);
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::setProvider(std::shared_ptr<ListItemProvider> prov)
|
|
{
|
|
if (prov != nullptr) {
|
|
provider = prov;
|
|
provider->list = this;
|
|
}
|
|
}
|
|
|
|
bool ListViewEngine::isEmpty() const noexcept
|
|
{
|
|
return elementsCount == 0;
|
|
}
|
|
|
|
void ListViewEngine::rebuildList(listview::RebuildType rebuildType, unsigned dataOffset, bool forceRebuild)
|
|
{
|
|
if (pageLoaded || forceRebuild) {
|
|
|
|
setElementsCount(provider->requestRecordsCount());
|
|
|
|
setup(rebuildType, dataOffset);
|
|
|
|
// If deletion operation caused last page to be removed request previous one.
|
|
if (startIndex != 0 && startIndex == elementsCount) {
|
|
requestPreviousPage();
|
|
}
|
|
else {
|
|
provider->requestRecords(startIndex, calculateLimit());
|
|
}
|
|
}
|
|
else {
|
|
rebuildRequests.push_front({rebuildType, dataOffset});
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::reSendLastRebuildRequest()
|
|
{
|
|
rebuildList(lastRebuildRequest.first, lastRebuildRequest.second, true);
|
|
}
|
|
|
|
void ListViewEngine::prepareFullRebuild()
|
|
{
|
|
setStartIndex();
|
|
storedFocusIndex = listview::nPos;
|
|
}
|
|
|
|
void ListViewEngine::prepareOnOffsetRebuild(unsigned dataOffset)
|
|
{
|
|
if (dataOffset < elementsCount) {
|
|
startIndex = dataOffset;
|
|
storedFocusIndex = listview::nPos;
|
|
}
|
|
else {
|
|
LOG_ERROR("Requested rebuild on index greater than elements count");
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::prepareInPlaceRebuild()
|
|
{
|
|
if (!body->empty()) {
|
|
storedFocusIndex = getFocusItemIndex();
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::prepareOnPageElementRebuild(unsigned dataOffset)
|
|
{
|
|
const auto maxItemsOnPage = calculateMaxItemsOnPage();
|
|
startIndex = (dataOffset / maxItemsOnPage) * maxItemsOnPage;
|
|
storedFocusIndex = dataOffset % maxItemsOnPage;
|
|
}
|
|
|
|
void ListViewEngine::setup(listview::RebuildType rebuildType, unsigned dataOffset)
|
|
{
|
|
switch (rebuildType) {
|
|
case listview::RebuildType::Full:
|
|
prepareFullRebuild();
|
|
break;
|
|
case listview::RebuildType::OnOffset:
|
|
prepareOnOffsetRebuild(dataOffset);
|
|
break;
|
|
case listview::RebuildType::InPlace:
|
|
prepareInPlaceRebuild();
|
|
break;
|
|
case listview::RebuildType::OnPageElement:
|
|
prepareOnPageElementRebuild(dataOffset);
|
|
break;
|
|
}
|
|
|
|
if (prepareRebuildCallback) {
|
|
prepareRebuildCallback();
|
|
setElementsCount(provider->requestRecordsCount());
|
|
}
|
|
|
|
lastRebuildRequest = {rebuildType, dataOffset};
|
|
|
|
body->setReverseOrder(false);
|
|
direction = listview::Direction::Bottom;
|
|
}
|
|
|
|
void ListViewEngine::onClose()
|
|
{
|
|
if (!body->empty()) {
|
|
storedFocusIndex = getFocusItemIndex();
|
|
}
|
|
clear();
|
|
}
|
|
|
|
unsigned ListViewEngine::getFocusItemIndex()
|
|
{
|
|
auto index = body->getFocusItemIndex();
|
|
|
|
if (direction == listview::Direction::Top) {
|
|
const int position = currentPageSize - 1 - index;
|
|
index = std::abs(position);
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
std::shared_ptr<ListItemProvider> ListViewEngine::getProvider()
|
|
{
|
|
return provider;
|
|
}
|
|
|
|
void ListViewEngine::reset()
|
|
{
|
|
clear();
|
|
setStartIndex();
|
|
body->setReverseOrder(false);
|
|
direction = listview::Direction::Bottom;
|
|
}
|
|
|
|
void ListViewEngine::clear()
|
|
{
|
|
body->setFocusItem(nullptr);
|
|
|
|
while (const auto el = body->children.back()) {
|
|
if (el->type == ItemType::LIST_ITEM) {
|
|
if (!dynamic_cast<ListItem *>(el)->deleteByList) {
|
|
body->removeWidget(el);
|
|
}
|
|
else {
|
|
body->erase(el);
|
|
}
|
|
}
|
|
else {
|
|
body->erase(el);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::refresh()
|
|
{
|
|
if (provider == nullptr) {
|
|
LOG_ERROR("ListView Data provider not exist");
|
|
return;
|
|
}
|
|
|
|
clear();
|
|
checkEmptyListCallbacks();
|
|
|
|
addItemsOnPage();
|
|
|
|
setFocus();
|
|
|
|
if (updateScrollCallback) {
|
|
updateScrollCallback(ListViewScrollUpdateData{startIndex,
|
|
currentPageSize,
|
|
elementsCount,
|
|
provider->getMinimalItemSpaceRequired(),
|
|
direction,
|
|
boundaries});
|
|
}
|
|
|
|
if (resizeScrollCallback) {
|
|
resizeScrollCallback();
|
|
}
|
|
|
|
pageLoaded = true;
|
|
|
|
// Check if there were queued rebuild Requests - if so rebuild list again.
|
|
if (!rebuildRequests.empty()) {
|
|
auto request = rebuildRequests.back();
|
|
rebuildRequests.pop_back();
|
|
rebuildList(request.first, request.second);
|
|
}
|
|
|
|
updateCountOfElementsAboveCurrentPage();
|
|
|
|
fillFirstPage();
|
|
}
|
|
|
|
void ListViewEngine::onProviderDataUpdate()
|
|
{
|
|
if (!renderFullList()) {
|
|
return;
|
|
}
|
|
|
|
refresh();
|
|
}
|
|
|
|
Order ListViewEngine::getOrderFromDirection() const noexcept
|
|
{
|
|
if (direction == listview::Direction::Bottom) {
|
|
return Order::Next;
|
|
}
|
|
|
|
return Order::Previous;
|
|
}
|
|
|
|
Order ListViewEngine::getOppositeOrderFromDirection() const noexcept
|
|
{
|
|
if (direction == listview::Direction::Bottom) {
|
|
return Order::Previous;
|
|
}
|
|
|
|
return Order::Next;
|
|
}
|
|
|
|
void ListViewEngine::setStartIndex()
|
|
{
|
|
if (orientation == listview::Orientation::TopBottom) {
|
|
startIndex = 0;
|
|
}
|
|
else {
|
|
startIndex = elementsCount;
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::recalculateStartIndex()
|
|
{
|
|
if (direction == listview::Direction::Top) {
|
|
startIndex = startIndex < currentPageSize ? 0 : startIndex - currentPageSize;
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::fillFirstPage()
|
|
{
|
|
// Check if first page is filled with items. If not reload page to be filled with items. Check for both
|
|
// Orientations.
|
|
if (orientation == listview::Orientation::TopBottom && direction == listview::Direction::Top &&
|
|
startIndex == 0) {
|
|
if (body->getPrimarySizeLeft() >= provider->getMinimalItemSpaceRequired()) {
|
|
focusOnLastItem = true;
|
|
|
|
if (checkFullRenderRequirementCallback) {
|
|
checkFullRenderRequirementCallback();
|
|
}
|
|
rebuildList();
|
|
}
|
|
}
|
|
|
|
if (orientation == listview::Orientation::BottomTop && direction == listview::Direction::Bottom &&
|
|
startIndex + currentPageSize == elementsCount) {
|
|
if (body->getPrimarySizeLeft() >= provider->getMinimalItemSpaceRequired()) {
|
|
focusOnLastItem = true;
|
|
|
|
if (checkFullRenderRequirementCallback) {
|
|
checkFullRenderRequirementCallback();
|
|
}
|
|
rebuildList();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::addItemsOnPage()
|
|
{
|
|
currentPageSize = 0;
|
|
|
|
while (const auto item = provider->getItem(getOrderFromDirection())) {
|
|
|
|
body->addWidget(item);
|
|
|
|
if (!item->visible) {
|
|
// In case model is tracking internal indexes -> undo last get.
|
|
if (requestFullListRender) {
|
|
const auto prevItem = provider->getItem(getOppositeOrderFromDirection());
|
|
delete prevItem; // Remove created item to prevent memory leak
|
|
}
|
|
break;
|
|
}
|
|
|
|
currentPageSize++;
|
|
}
|
|
|
|
recalculateStartIndex();
|
|
}
|
|
|
|
bool ListViewEngine::renderFullList()
|
|
{
|
|
if (!requestFullListRender) {
|
|
return true;
|
|
}
|
|
|
|
if (elementsCount != 0 && !requestCompleteData) {
|
|
requestCompleteData = true;
|
|
provider->requestRecords(0, elementsCount);
|
|
return false;
|
|
}
|
|
|
|
if (requestCompleteData) {
|
|
|
|
unsigned page = 0;
|
|
auto pageStartIndex = 0;
|
|
|
|
clear();
|
|
|
|
while (true) {
|
|
|
|
addItemsOnPage();
|
|
|
|
if (currentPageSize == 0) {
|
|
break;
|
|
}
|
|
|
|
if (currentPageSize + pageStartIndex == elementsCount) {
|
|
break;
|
|
}
|
|
|
|
page += 1;
|
|
pageStartIndex += currentPageSize;
|
|
|
|
clear();
|
|
}
|
|
|
|
clear();
|
|
requestCompleteData = false;
|
|
requestFullListRender = false;
|
|
|
|
if (lastRebuildRequest.first == listview::RebuildType::Full) {
|
|
if (orientation == listview::Orientation::TopBottom) {
|
|
if (setupScrollCallback) {
|
|
setupScrollCallback(ListViewScrollSetupData{startIndex, 0, (page + 1)});
|
|
}
|
|
}
|
|
else {
|
|
if (setupScrollCallback) {
|
|
setupScrollCallback(ListViewScrollSetupData{startIndex, page, (page + 1)});
|
|
}
|
|
}
|
|
}
|
|
|
|
updateCountOfElementsAboveCurrentPage();
|
|
|
|
reSendLastRebuildRequest();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} // namespace gui
|
|
|
|
void ListViewEngine::setFocus()
|
|
{
|
|
if (storedFocusIndex != listview::nPos) {
|
|
if (!body->setFocusOnElement(storedFocusIndex)) {
|
|
body->setFocusOnLastElement();
|
|
}
|
|
}
|
|
|
|
if (focusOnLastItem) {
|
|
body->setFocusOnLastElement();
|
|
focusOnLastItem = false;
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::onElementsCountChanged(unsigned count)
|
|
{
|
|
if (elementsCount == 0 || count == 0) {
|
|
shouldCallEmptyListCallbacks = true;
|
|
}
|
|
|
|
elementsCount = count;
|
|
|
|
if (checkFullRenderRequirementCallback) {
|
|
checkFullRenderRequirementCallback();
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::checkEmptyListCallbacks()
|
|
{
|
|
if (shouldCallEmptyListCallbacks) {
|
|
if (isEmpty()) {
|
|
if (emptyListCallback) {
|
|
emptyListCallback();
|
|
}
|
|
}
|
|
else if (notEmptyListCallback) {
|
|
notEmptyListCallback();
|
|
}
|
|
shouldCallEmptyListCallbacks = false;
|
|
}
|
|
}
|
|
|
|
void ListViewEngine::recalculateOnBoxRequestedResize()
|
|
{
|
|
if (currentPageSize != body->getVisibleChildrenCount()) {
|
|
|
|
const unsigned diff = currentPageSize < body->getVisibleChildrenCount()
|
|
? 0
|
|
: currentPageSize - body->getVisibleChildrenCount();
|
|
currentPageSize = body->getVisibleChildrenCount();
|
|
|
|
if (direction == listview::Direction::Top) {
|
|
startIndex += diff;
|
|
}
|
|
else {
|
|
startIndex = startIndex < diff ? 0 : startIndex - diff;
|
|
}
|
|
|
|
if (checkFullRenderRequirementCallback) {
|
|
checkFullRenderRequirementCallback();
|
|
}
|
|
|
|
rebuildList();
|
|
}
|
|
}
|
|
|
|
unsigned ListViewEngine::calculateMaxItemsOnPage()
|
|
{
|
|
assert(provider->getMinimalItemSpaceRequired() != 0);
|
|
const auto count = body->getPrimarySize() / provider->getMinimalItemSpaceRequired();
|
|
return count;
|
|
}
|
|
|
|
unsigned ListViewEngine::calculateLimit(listview::Direction value)
|
|
{
|
|
const auto minLimit = std::max(2 * currentPageSize, calculateMaxItemsOnPage());
|
|
|
|
if (value == listview::Direction::Bottom) {
|
|
return (minLimit + startIndex <= elementsCount) ? minLimit : (elementsCount - startIndex);
|
|
}
|
|
return std::min(minLimit, startIndex);
|
|
}
|
|
|
|
bool ListViewEngine::requestNextPage()
|
|
{
|
|
if (startIndex + currentPageSize >= elementsCount && boundaries == Boundaries::Continuous) {
|
|
|
|
startIndex = 0;
|
|
}
|
|
else if (startIndex + currentPageSize >= elementsCount && boundaries == Boundaries::Fixed) {
|
|
|
|
return false;
|
|
}
|
|
else {
|
|
|
|
startIndex = startIndex <= elementsCount - currentPageSize ? startIndex + currentPageSize
|
|
: elementsCount - (elementsCount - startIndex);
|
|
}
|
|
|
|
direction = listview::Direction::Bottom;
|
|
body->setReverseOrder(false);
|
|
pageLoaded = false;
|
|
storedFocusIndex = listview::nPos;
|
|
provider->requestRecords(startIndex, calculateLimit());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ListViewEngine::requestPreviousPage()
|
|
{
|
|
auto topFetchIndex = 0;
|
|
auto limit = 0;
|
|
|
|
if (startIndex == 0 && boundaries == Boundaries::Continuous) {
|
|
|
|
startIndex = elementsCount;
|
|
if (elementsCount > currentPageSize && fetchType == listview::FetchType::Fixed) {
|
|
|
|
auto calculateFixedFill =
|
|
elementsCount % currentPageSize != 0 ? elementsCount % currentPageSize : currentPageSize;
|
|
topFetchIndex = elementsCount - calculateFixedFill;
|
|
}
|
|
else {
|
|
topFetchIndex = elementsCount - calculateLimit(listview::Direction::Top);
|
|
}
|
|
|
|
limit = calculateLimit(listview::Direction::Top);
|
|
}
|
|
else if (startIndex == 0 && boundaries == Boundaries::Fixed) {
|
|
|
|
return false;
|
|
}
|
|
else {
|
|
|
|
limit = calculateLimit(listview::Direction::Top);
|
|
topFetchIndex = startIndex < calculateLimit(listview::Direction::Top)
|
|
? 0
|
|
: startIndex - calculateLimit(listview::Direction::Top);
|
|
}
|
|
|
|
direction = listview::Direction::Top;
|
|
body->setReverseOrder(true);
|
|
pageLoaded = false;
|
|
storedFocusIndex = listview::nPos;
|
|
|
|
provider->requestRecords(topFetchIndex, limit);
|
|
|
|
return true;
|
|
}
|
|
|
|
void ListViewEngine::updateCountOfElementsAboveCurrentPage()
|
|
{
|
|
const auto countOfElementsAboveCurrentPage = startIndex;
|
|
if (onElementsAboveOfCurrentPageChangeCallback) {
|
|
onElementsAboveOfCurrentPageChangeCallback(countOfElementsAboveCurrentPage);
|
|
}
|
|
}
|
|
} /* namespace gui */
|