~aleteoryx/muditaos

40afdb710da7907af6faf2843a2fd2182b3272d0 — Dawid Wojtas 1 year, 11 months ago c0e13a8
[BH-1920] Labels with correct page and song

This is reimplementation ListViewWithLabels
which is able to set correct page with
focused item.
M module-db/Tables/MultimediaFilesTable.cpp => module-db/Tables/MultimediaFilesTable.cpp +6 -17
@@ 391,23 391,12 @@ namespace db::multimedia_files
                                                             const std::string &recordPath,
                                                             SortingBy sorting) -> SortedRecord
    {
        const std::string query = "SELECT * FROM ("
                                  "      SELECT"
                                  "          ROW_NUMBER () OVER ("
                                  "              ORDER BY " +
                                  getSorting(sorting) +
                                  "          ) offset,"
                                  "          path"
                                  "      FROM"
                                  "          files"
                                  "      WHERE path LIKE '" +
                                  folderPath + "%%'" +
                                  "  )"
                                  "  WHERE"
                                  "      path='" +
                                  recordPath + "'";

        std::unique_ptr<QueryResult> retQuery = db->query(query.c_str());
        std::unique_ptr<QueryResult> retQuery =
            db->query("SELECT * FROM ( SELECT ROW_NUMBER () OVER ( ORDER BY %q ) offset, "
                      " path FROM files WHERE path LIKE '%q%%' ) WHERE path='%q'",
                      getSorting(sorting).c_str(),
                      folderPath.c_str(),
                      recordPath.c_str());
        if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
            return SortedRecord{.path = recordPath, .offset = 0};
        }

M module-gui/gui/widgets/ListViewEngine.cpp => module-gui/gui/widgets/ListViewEngine.cpp +1 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// 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"

M products/BellHybrid/apps/common/CMakeLists.txt => products/BellHybrid/apps/common/CMakeLists.txt +2 -2
@@ 65,7 65,7 @@ target_sources(application-bell-common
        src/widgets/LayoutVertical.cpp
        src/widgets/ClockVertical.cpp
        src/widgets/ListViewWithLabels.cpp
        src/widgets/LabelListItem.cpp
        src/widgets/LabelMarkerItem.cpp
        src/widgets/LabelOptionWithTick.cpp

        src/options/BellOptionWindow.cpp


@@ 151,7 151,7 @@ target_sources(application-bell-common
        include/common/widgets/LayoutVertical.hpp
        include/common/widgets/ClockVertical.hpp
        include/common/widgets/ListViewWithLabels.hpp
        include/common/widgets/LabelListItem.hpp
        include/common/widgets/LabelMarkerItem.hpp
        include/common/widgets/LabelOptionWithTick.hpp

        include/common/options/BellOptionWindow.hpp

M products/BellHybrid/apps/common/include/common/SoundsRepository.hpp => products/BellHybrid/apps/common/include/common/SoundsRepository.hpp +2 -0
@@ 50,6 50,7 @@ class AbstractSoundsRepository
                               const OnGetMusicFilesListCallback &callback) = 0;

    virtual std::uint32_t getFilesCount() = 0;
    virtual std::uint32_t getFilesCountFromPath(const std::string &filesPath) = 0;
    virtual void updateFilesCount()       = 0;
};



@@ 75,6 76,7 @@ class SoundsRepository : public AbstractSoundsRepository, public app::AsyncCallb
                       const OnGetMusicFilesListCallback &viewUpdateCallback) override;

    std::uint32_t getFilesCount() override;
    std::uint32_t getFilesCountFromPath(const std::string &filesPath) override;
    void updateFilesCount() override;

  private:

M products/BellHybrid/apps/common/include/common/models/SongsModel.hpp => products/BellHybrid/apps/common/include/common/models/SongsModel.hpp +4 -3
@@ 4,13 4,13 @@
#pragma once

#include <common/SoundsRepository.hpp>
#include <common/widgets/LabelListItem.hpp>
#include <common/widgets/LabelMarkerItem.hpp>
#include <apps-common/models/SongsModelInterface.hpp>
#include <gui/widgets/ListItemProvider.hpp>

namespace app
{
    using LabelsWithPaths = std::map<std::string, std::string>;
    using LabelsWithPaths = std::vector<std::pair<std::string, std::string>>;

    class SongsProvider : public app::DatabaseModel<db::multimedia_files::MultimediaFilesRecord>,
                          public gui::ListItemProvider


@@ 55,6 55,7 @@ namespace app
        auto nextRecordExists(gui::Order order) -> bool;

        auto updateCurrentlyChosenRecordPath(const std::string &path) -> void;
        auto getLabelsFilesCount() -> std::vector<std::pair<std::string, std::uint32_t>>;
        [[nodiscard]] auto getCurrentlyChosenRecordPath() const -> std::string;

      private:


@@ 62,7 63,7 @@ namespace app
                                  unsigned repoRecordsCount) -> bool;
        [[nodiscard]] auto updateRecords(std::vector<db::multimedia_files::MultimediaFilesRecord> records)
            -> bool override;
        auto getLabelFromPath(const std::string &path) -> gui::ListLabel;
        auto getLabelFromPath(const std::string &path) -> std::string;

        ApplicationCommon *application{nullptr};
        std::unique_ptr<AbstractSoundsRepository> songsRepository;

R products/BellHybrid/apps/common/include/common/widgets/LabelListItem.hpp => products/BellHybrid/apps/common/include/common/widgets/LabelMarkerItem.hpp +0 -11
@@ 9,17 9,6 @@

namespace gui
{
    class LabelListItem : public ListItem
    {
      private:
        ListLabel label{};

      public:
        explicit LabelListItem(ListLabel label);
        virtual ~LabelListItem() = default;
        ListLabel getLabel();
    };

    class LabelMarkerItem : public ListItem
    {
      public:

M products/BellHybrid/apps/common/include/common/widgets/LabelOptionWithTick.hpp => products/BellHybrid/apps/common/include/common/widgets/LabelOptionWithTick.hpp +2 -2
@@ 17,7 17,7 @@ namespace gui::option
            Hide
        };

        LabelOptionWithTick(ListLabel label,
        LabelOptionWithTick(const std::string &label,
                            const UTF8 &text,
                            TickState tickState,
                            std::function<bool(Item &)> activatedCallback,


@@ 30,7 30,7 @@ namespace gui::option
        auto prepareLabelOption(ListItem *item) const -> void;
        auto getAdjustedText(TextFixedSize *textItem) const -> UTF8;

        ListLabel label;
        std::string label;
        TickState tickState;
    };
} // namespace gui::option

M products/BellHybrid/apps/common/include/common/widgets/ListViewWithLabels.hpp => products/BellHybrid/apps/common/include/common/widgets/ListViewWithLabels.hpp +11 -12
@@ 13,8 13,6 @@ namespace gui
    class ListItemProvider;
    class LabelMarkerItem;

    using ListLabel = std::optional<std::string>;

    class ListViewWithLabels : public ListViewWithArrows
    {
      public:


@@ 25,19 23,20 @@ namespace gui
                           unsigned int h,
                           std::shared_ptr<ListItemProvider> prov);

        void reset() override;

      private:
        [[nodiscard]] std::size_t getSlotsLeft() const;
        void addItemsOnPage() override;
        void addLabelMarker(ListItem *item);
        void updateState(ListLabel newMarker);
        LabelMarkerItem *createMarkerItem(ListLabel label);

        ListLabel current{std::nullopt};
        ListLabel previous{std::nullopt};
        ListLabel currentMarker{std::nullopt};
        void addItems();
        void addLabelItem();
        void getLabels();
        std::uint32_t getLabelsCount(unsigned int index);
        LabelMarkerItem *createMarkerItem(const std::string &label);

        std::vector<std::pair<std::string, std::uint32_t>> labelFiles;
        unsigned int currentFocusIndex{0};
        unsigned int hiddenItemIndex{0};
        std::uint32_t itemsOnPage{0};
        bool labelAdded{false};
        std::uint32_t labelsCount{0};
        bool wasSetFocus{false};
    };
} // namespace gui

M products/BellHybrid/apps/common/src/SoundsRepository.cpp => products/BellHybrid/apps/common/src/SoundsRepository.cpp +10 -0
@@ 185,6 185,16 @@ std::uint32_t SoundsRepository::getFilesCount()
        paths.begin(), paths.end(), 0, [](const std::uint32_t sum, const auto &record) { return sum + record.count; });
}

std::uint32_t SoundsRepository::getFilesCountFromPath(const std::string &filesPath)
{
    for (const auto &path : paths) {
        if (filesPath == path.prefix) {
            return path.count;
        }
    }
    return 0;
}

void SoundsRepository::updateFilesCount()
{
    for (const auto &path : paths) {

M products/BellHybrid/apps/common/src/models/SongsModel.cpp => products/BellHybrid/apps/common/src/models/SongsModel.cpp +13 -3
@@ 23,7 23,7 @@ namespace app

    auto SongsModel::getMinimalItemSpaceRequired() const -> unsigned
    {
        return style::bell_options::h + 2 * style::bell_options::option_margin;
        return style::bell_options::h + 3 * style::bell_options::option_margin;
    }

    auto SongsModel::getItem(gui::Order order) -> gui::ListItem *


@@ 104,14 104,24 @@ namespace app
        return true;
    }

    auto SongsModel::getLabelFromPath(const std::string &path) -> gui::ListLabel
    auto SongsModel::getLabelFromPath(const std::string &path) -> std::string
    {
        for (const auto &[label, pathPrefix] : pathPrefixes) {
            if (path.find(pathPrefix) != std::string::npos) {
                return label;
            }
        }
        return std::nullopt;
        return {};
    }

    auto SongsModel::getLabelsFilesCount() -> std::vector<std::pair<std::string, std::uint32_t>>
    {
        std::vector<std::pair<std::string, std::uint32_t>> labelWithFilesCount;
        for (const auto &[label, pathPrefix] : pathPrefixes) {
            const auto count = songsRepository->getFilesCountFromPath(pathPrefix);
            labelWithFilesCount.push_back({label, count});
        }
        return labelWithFilesCount;
    }

    auto SongsModel::updateCurrentlyChosenRecordPath(const std::string &path) -> void

R products/BellHybrid/apps/common/src/widgets/LabelListItem.cpp => products/BellHybrid/apps/common/src/widgets/LabelMarkerItem.cpp +1 -9
@@ 1,19 1,11 @@
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <common/widgets/LabelListItem.hpp>
#include <common/widgets/LabelMarkerItem.hpp>
#include "common/options/OptionBellMenu.hpp"

namespace gui
{
    LabelListItem::LabelListItem(ListLabel label) : label{std::move(label)}
    {}

    ListLabel LabelListItem::getLabel()
    {
        return label;
    }

    LabelMarkerItem::LabelMarkerItem(const UTF8 &labelText)
    {
        constexpr auto linesMaxNumber{1U};

M products/BellHybrid/apps/common/src/widgets/LabelOptionWithTick.cpp => products/BellHybrid/apps/common/src/widgets/LabelOptionWithTick.cpp +3 -3
@@ 2,11 2,11 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <common/widgets/LabelOptionWithTick.hpp>
#include <common/widgets/LabelListItem.hpp>
#include <common/widgets/LabelMarkerItem.hpp>

namespace gui::option
{
    LabelOptionWithTick::LabelOptionWithTick(ListLabel label,
    LabelOptionWithTick::LabelOptionWithTick(const std::string &label,
                                             const UTF8 &text,
                                             TickState tickState,
                                             std::function<bool(Item &)> activatedCallback,


@@ 18,7 18,7 @@ namespace gui::option

    auto LabelOptionWithTick::build() const -> ListItem *
    {
        auto labelOption = new LabelListItem(label);
        auto labelOption = new ListItem();
        prepareLabelOption(labelOption);
        return labelOption;
    }

M products/BellHybrid/apps/common/src/widgets/ListViewWithLabels.cpp => products/BellHybrid/apps/common/src/widgets/ListViewWithLabels.cpp +136 -90
@@ 2,9 2,14 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <common/widgets/ListViewWithLabels.hpp>
#include <common/widgets/LabelListItem.hpp>
#include <common/widgets/LabelMarkerItem.hpp>
#include <common/widgets/LabelOptionWithTick.hpp>
#include <common/models/SongsModel.hpp>

namespace
{
    constexpr auto maxItemsOnPage{4U};
}
namespace gui
{
    ListViewWithLabels::ListViewWithLabels(Item *parent,


@@ 16,127 21,168 @@ namespace gui
        : ListViewWithArrows(parent, x, y, w, h, std::move(prov))
    {
        body->dimensionChangedCallback = [&](gui::Item &, const BoundingBox &newDim) -> bool { return true; };
    }

    void ListViewWithLabels::addItemsOnPage()
    {
        currentPageSize = 0;
        itemsOnPage     = 0;
        labelAdded      = false;
        updateScrollCallback = [this](ListViewScrollUpdateData data) {
            if (currentPageSize + data.startIndex < data.elementsCount) {
                listOverlay->lastBox->setVisible(true);
            }
            else {
                listOverlay->lastBox->setVisible(false);
            }

        ListItem *item;
        while ((item = provider->getItem(getOrderFromDirection())) != nullptr) {
            /* If direction is top-to-bottom, add label mark before adding relaxation item. */
            if (direction == listview::Direction::Bottom) {
                addLabelMarker(item);
            if (data.startIndex == 0 && titleBody) {
                titleBody->setVisible(true);
                arrowTop->setVisible(false);
                listOverlay->firstBox->setVisible(true);
            }
            /* Check if new item fits, if it does - add it, if not - handle label insertion
             * case for bottom-to-top navigation direction. */
            if (getSlotsLeft() > 0) {
                body->addWidget(item);
                itemsOnPage++;
            else if (data.startIndex > 1) {
                if (titleBody) {
                    titleBody->setVisible(false);
                }
                arrowTop->setVisible(true);
                listOverlay->firstBox->setVisible(true);
            }
            else {
                /* Add invisible item to list to avoid memory leak */
                item->setVisible(false);
                body->addWidget(item);
                break;
                listOverlay->firstBox->setVisible(false);
            }
            /* If direction is bottom-to-top, add label mark after adding relaxation item. */
            if (direction == listview::Direction::Top) {
                addLabelMarker(item);
            }
            currentPageSize++;

            listOverlay->resizeItems();
            // Second resize is needed as we need to first apply max size for center box and next extra margins.
            listOverlay->resizeItems();
        };
    }

    void ListViewWithLabels::addLabelItem()
    {
        if (!labelsCount) {
            return;
        }

        recalculateStartIndex();
        const std::int32_t size = direction == listview::Direction::Top ? -currentPageSize : currentPageSize;

        if (!labelAdded) {
            currentMarker.reset();
        auto position = 0U;
        for (auto i = 0U; i < labelsCount; ++i) {
            // First label always starts from 0
            if (i == 0) {
                position = 0;
            }
            else {
                position += labelFiles[i - 1].second;
            }
            const auto &[labelName, filesCount] = labelFiles[i];
            const auto isSpace                  = (getSlotsLeft() > 0);
            if (((startIndex + size) == position) && (filesCount > 0) && isSpace) {
                // Make sure that hidden item doesn't allow
                // to display label on the next page
                if ((hiddenItemIndex == position) && (position > 0)) {
                    hiddenItemIndex = 0;
                    break;
                }
                body->addWidget(createMarkerItem(labelName));
                itemsOnPage++;
            }
        }
    }

    LabelMarkerItem *ListViewWithLabels::createMarkerItem(ListLabel label)
    void ListViewWithLabels::getLabels()
    {
        if (label.has_value()) {
            const auto &labelString = UTF8(utils::translate(label.value()));
            return new LabelMarkerItem(labelString);
        const auto songsProvider = std::dynamic_pointer_cast<app::SongsModel>(provider);
        if (songsProvider) {
            labelFiles  = songsProvider->getLabelsFilesCount();
            labelsCount = labelFiles.size();
        }
        return new LabelMarkerItem(UTF8(""));
    }

    void ListViewWithLabels::addLabelMarker(ListItem *item)
    std::uint32_t ListViewWithLabels::getLabelsCount(unsigned int index)
    {
        const auto labelListItem = dynamic_cast<gui::LabelListItem *>(item);
        if (labelListItem == nullptr) {
            return;
        };
        previous = current;
        current  = labelListItem->getLabel();

        switch (direction) {
        case listview::Direction::Bottom:
            if (current != previous && current != currentMarker) {
                body->addWidget(createMarkerItem(*current));
                updateState(current);
        auto total  = 0U;
        auto offset = 0U;
        for (const auto &[_, files] : labelFiles) {
            if (index > offset) {
                total++;
            }
            break;

        case listview::Direction::Top:
            if (current != previous && previous != currentMarker) {
                const auto initialSlotsLeft = getSlotsLeft();

                body->removeWidget(labelListItem);
                body->addWidget(createMarkerItem(*previous));
                updateState(previous);

                /* Add item to body even if it won't fit to avoid manual memory
                 * management for item, but apply correction to currentPageSize
                 * if it is not visible. */
                body->addWidget(labelListItem);
            offset += files;
        }
        return total;
    }

                if (initialSlotsLeft == 0) {
                    currentPageSize--;
                    itemsOnPage--;
                }
    void ListViewWithLabels::addItemsOnPage()
    {
        getLabels();

        const auto rebuildType = lastRebuildRequest.first;
        if ((storedFocusIndex != listview::nPos) && (rebuildType != listview::RebuildType::InPlace)) {
            const auto totalLabels = getLabelsCount(startIndex + storedFocusIndex);
            const auto nextPage    = (totalLabels + storedFocusIndex) / maxItemsOnPage;
            currentFocusIndex      = storedFocusIndex + totalLabels;
            if (nextPage) {
                startIndex += maxItemsOnPage - totalLabels;
                currentFocusIndex %= maxItemsOnPage;
            }
            else if (startIndex) {
                startIndex -= totalLabels;
            }
            else {
                /* This is bad, as it limits usage of this widget to just SongsModel */
                const auto songsProvider = std::dynamic_pointer_cast<app::SongsModel>(provider);
                if (songsProvider == nullptr) {
                    break;
                }
                const auto nextItemExists = songsProvider->nextRecordExists(getOrderFromDirection());
                if (!nextItemExists && getSlotsLeft() == 1) {
                    body->addWidget(createMarkerItem(current));
                    updateState(current);
                }
                currentFocusIndex -= totalLabels;
            }
            requestNextPage();
            wasSetFocus = true;
        }
        else {
            currentPageSize = 0;
            itemsOnPage     = 0;
            if (wasSetFocus) {
                storedFocusIndex = currentFocusIndex;
                wasSetFocus      = false;
            }
            break;
            addItems();
        }
    }

    std::size_t ListViewWithLabels::getSlotsLeft() const
    void ListViewWithLabels::addItems()
    {
        constexpr auto maxItemDisplayed{4U};
        if (itemsOnPage > maxItemDisplayed) {
            return 0;
        ListItem *item;
        while (true) {
            item = provider->getItem(getOrderFromDirection());
            if (item == nullptr) {
                // Add label if the direction is Top and we are on the first page
                // So we don't get more songs items but we need to add a label
                addLabelItem();
                break;
            }
            addLabelItem();
            // Check if new item fits, if it does - add it
            if (getSlotsLeft() > 0) {
                body->addWidget(item);
                itemsOnPage++;
            }
            else {
                // Add invisible item to list to avoid memory leak
                item->setVisible(false);
                body->addWidget(item);
                // Save the hidden index for calculating label position
                hiddenItemIndex = startIndex;
                if (direction == listview::Direction::Bottom) {
                    hiddenItemIndex += currentPageSize;
                }
                break;
            }
            currentPageSize++;
        }
        return maxItemDisplayed - itemsOnPage;
        recalculateStartIndex();
    }

    void ListViewWithLabels::reset()
    LabelMarkerItem *ListViewWithLabels::createMarkerItem(const std::string &label)
    {
        currentMarker.reset();
        previous.reset();
        current.reset();
        ListViewEngine::reset();
        const auto &labelString = UTF8(utils::translate(label));
        return new LabelMarkerItem(labelString);
    }

    void ListViewWithLabels::updateState(ListLabel marker)
    std::size_t ListViewWithLabels::getSlotsLeft() const
    {
        currentMarker = std::move(marker);
        itemsOnPage++;
        labelAdded = true;
        if (itemsOnPage > maxItemsOnPage) {
            return 0;
        }
        return maxItemsOnPage - itemsOnPage;
    }
} // namespace gui