~aleteoryx/muditaos

46bdedf9cd614f843a48cfc677d554154c2a1f68 — Mateusz Piesta 3 years ago 40cddc1
[BH-1350] Meditation timer update

Meditation timer application update.
Refactored gui spinners, list items.
92 files changed, 2128 insertions(+), 1504 deletions(-)

M image/assets/lang/Deutsch.json
M image/assets/lang/English.json
M image/assets/lang/Espanol.json
M image/assets/lang/Francais.json
M image/assets/lang/Polski.json
M module-apps/apps-common/CMakeLists.txt
M module-apps/apps-common/widgets/TextSpinnerBox.cpp
M module-apps/apps-common/widgets/TextSpinnerBox.hpp
M module-apps/apps-common/widgets/TimeSetFmtSpinner.cpp
M module-apps/apps-common/widgets/TimeSetFmtSpinner.hpp
M module-apps/apps-common/widgets/TimeSetSpinner.cpp
D module-apps/apps-common/widgets/spinners/GenericSpinner.hpp
M module-apps/apps-common/widgets/spinners/ItemSpinner.hpp
A module-apps/apps-common/widgets/spinners/Model.hpp
D module-apps/apps-common/widgets/spinners/SpinnerContents.hpp
D module-apps/apps-common/widgets/spinners/SpinnerPolicies.hpp
M module-apps/apps-common/widgets/spinners/Spinners.hpp
A module-apps/apps-common/widgets/spinners/StringOutputSpinner.hpp
M module-apps/tests/CMakeLists.txt
A module-apps/tests/test-Model.cpp
M module-bsp/board/rt1051/bsp/audio/CodecMAX98090.hpp
M products/BellHybrid/BellHybridMain.cpp
M products/BellHybrid/apps/application-bell-background-sounds/windows/BGSoundsTimerSelectWindow.cpp
M products/BellHybrid/apps/application-bell-background-sounds/windows/BGSoundsTimerSelectWindow.hpp
M products/BellHybrid/apps/application-bell-background-sounds/windows/BGSoundsVolumeWindow.cpp
M products/BellHybrid/apps/application-bell-main/windows/BellMainMenuWindow.cpp
M products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt
R products/BellHybrid/apps/application-bell-meditation-timer/{ApplicationBellMeditationTimer => MeditationTimer}.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/data/Contract.hpp
M products/BellHybrid/apps/application-bell-meditation-timer/data/MeditationCommon.hpp
R products/BellHybrid/apps/application-bell-meditation-timer/include/application-bell-meditation-timer/{ApplicationBellMeditationTimer => MeditationTimer}.hpp
A products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeInterval.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeInterval.hpp
A products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeVolume.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeVolume.hpp
A products/BellHybrid/apps/application-bell-meditation-timer/models/StartDelay.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/models/StartDelay.hpp
D products/BellHybrid/apps/application-bell-meditation-timer/presenter/IntervalChimePresenter.cpp
D products/BellHybrid/apps/application-bell-meditation-timer/presenter/IntervalChimePresenter.hpp
M products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationProgressPresenter.cpp
M products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationProgressPresenter.hpp
M products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationTimerPresenter.cpp
M products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationTimerPresenter.hpp
M products/BellHybrid/apps/application-bell-meditation-timer/presenter/ReadyGoingPresenter.cpp
M products/BellHybrid/apps/application-bell-meditation-timer/presenter/ReadyGoingPresenter.hpp
A products/BellHybrid/apps/application-bell-meditation-timer/presenter/SettingsPresenter.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/presenter/SettingsPresenter.hpp
A products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp
D products/BellHybrid/apps/application-bell-meditation-timer/windows/IntervalChimeWindow.cpp
D products/BellHybrid/apps/application-bell-meditation-timer/windows/IntervalChimeWindow.hpp
A products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationMainWindow.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationMainWindow.hpp
M products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationRunningWindow.cpp
M products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationTimerWindow.cpp
M products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationTimerWindow.hpp
M products/BellHybrid/apps/application-bell-meditation-timer/windows/ReadyGoingWindow.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/windows/SettingsWindow.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/windows/SettingsWindow.hpp
A products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp
M products/BellHybrid/apps/application-bell-powernap/data/PowerNapListItem.cpp
M products/BellHybrid/apps/application-bell-settings/ApplicationBellSettings.cpp
M products/BellHybrid/apps/application-bell-settings/models/FrontlightListItemProvider.cpp
M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/AlarmSettingsListItemProvider.cpp
M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/BedtimeSettingsListItemProvider.cpp
M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/PrewakeUpListItemProvider.cpp
M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/SnoozeListItemProvider.cpp
M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/SnoozeSettingsModel.cpp
M products/BellHybrid/apps/application-bell-settings/presenter/BedtimeSettingsPresenter.cpp
M products/BellHybrid/apps/application-bell-settings/presenter/BedtimeSettingsPresenter.hpp
M products/BellHybrid/apps/application-bell-settings/widgets/TemperatureUnitListItem.cpp
M products/BellHybrid/apps/application-bell-settings/widgets/TemperatureUnitListItem.hpp
M products/BellHybrid/apps/application-bell-settings/widgets/TimeFormatSetListItem.cpp
M products/BellHybrid/apps/application-bell-settings/widgets/TimeFormatSetListItem.hpp
M products/BellHybrid/apps/application-bell-settings/windows/BellSettingsBedtimeToneWindow.cpp
M products/BellHybrid/apps/application-bell-settings/windows/BellSettingsBedtimeToneWindow.hpp
M products/BellHybrid/apps/application-bell-settings/windows/BellSettingsLanguageWindow.cpp
M products/BellHybrid/apps/application-bell-settings/windows/BellSettingsLanguageWindow.hpp
M products/BellHybrid/apps/common/include/common/BellListItemProvider.hpp
M products/BellHybrid/apps/common/include/common/LanguageUtils.hpp
M products/BellHybrid/apps/common/include/common/widgets/ListItems.hpp
A products/BellHybrid/apps/common/include/common/widgets/list_items/Fraction.hpp
A products/BellHybrid/apps/common/include/common/widgets/list_items/NumberWithSuffix.hpp
A products/BellHybrid/apps/common/include/common/widgets/list_items/Numeric.hpp
A products/BellHybrid/apps/common/include/common/widgets/list_items/OnOff.hpp
A products/BellHybrid/apps/common/include/common/widgets/list_items/Text.hpp
A products/BellHybrid/apps/common/include/common/widgets/list_items/details.hpp
M products/BellHybrid/apps/common/src/LanguageUtils.cpp
M products/BellHybrid/apps/common/src/models/SettingsModel.cpp
M products/BellHybrid/apps/common/src/widgets/ListItems.cpp
M products/BellHybrid/apps/common/src/widgets/TimeSetSpinnerVertical.cpp
M image/assets/lang/Deutsch.json => image/assets/lang/Deutsch.json +1 -2
@@ 678,10 678,9 @@
  "app_bell_settings_factory_reset": "Zurücksetzen",
  "app_bell_settings_display_factory_reset_confirmation": "Zurücksetzen?",
  "app_bell_meditation_timer": "Meditation",
  "app_bell_meditation_interval_chime": "Intervallschall",
  "app_bell_meditation_chime_interval": "Intervallschall",
  "app_bell_meditation_progress": "Meditation",
  "app_bell_meditation_interval_none": "kein",
  "app_bell_meditation_interval_every_x_minutes": "jede %0",
  "app_bell_meditation_put_down_and_wait": "<text>Legen Sie Mudita Harmony<br>ab und warten Sie auf den Gong</text>",
  "app_bell_meditation_thank_you_for_session": "<text>Danke für<br>die Sitzung</text>"
}

M image/assets/lang/English.json => image/assets/lang/English.json +11 -2
@@ 83,6 83,10 @@
  "common_minutes_lower": "minutes",
  "common_minutes_lower_genitive": "minutes",
  "common_minute_short": "min",
  "common_second_lower": "second",
  "common_seconds_lower": "seconds",
  "common_seconds_lower_genitive": "seconds",
  "common_second_short": "sec",
  "common_paused": "Paused",
  "common_text_copy": "Copy text",
  "common_text_paste": "Paste text",


@@ 610,10 614,15 @@
  "app_bellmain_settings": "Settings",
  "app_bellmain_main_window_title": "Mudita Harmony",
  "app_bell_meditation_timer": "Meditation",
  "app_bell_meditation_interval_chime": "Interval chime",
  "app_bell_meditation_settings": "Settings",
  "app_bell_meditation_start": "Meditate now",
  "app_bell_meditation_statistics": "Statistics",
  "app_bell_meditation_chime_volume": "Chime volume",
  "app_bell_meditation_chime_interval": "Chime interval",
  "app_bell_meditation_chime_interval_bottom": "of the meditation",
  "app_bell_meditation_start_delay": "Start delay",
  "app_bell_meditation_progress": "Meditation timer",
  "app_bell_meditation_interval_none": "None",
  "app_bell_meditation_interval_every_x_minutes": "every %0",
  "app_bell_meditation_put_down_and_wait": "<text>Put down Mudita Harmony<br>and wait for the gong</text>",
  "app_bell_meditation_thank_you_for_session": "<text>Thank you for<br>the session</text>",
  "app_bell_onboarding_welcome_message": "<text>Mudita Harmony<br/>is switched OFF</text>",

M image/assets/lang/Espanol.json => image/assets/lang/Espanol.json +1 -2
@@ 678,10 678,9 @@
  "app_bell_settings_factory_reset": "Restablecer",
  "app_bell_settings_display_factory_reset_confirmation": "<text>¿Restablecer la configuración?</text>",
  "app_bell_meditation_timer": "Meditación",
  "app_bell_meditation_interval_chime": "Intervalo",
  "app_bell_meditation_chime_interval": "Intervalo",
  "app_bell_meditation_progress": "Meditación",
  "app_bell_meditation_interval_none": "Ninguno",
  "app_bell_meditation_interval_every_x_minutes": "cada %0",
  "app_bell_meditation_put_down_and_wait": "<text>Posicione Mudita Harmony<br>y espere el gong</text>",
  "app_bell_meditation_thank_you_for_session": "<text>Gracias<br>por la sesión</text>"
}

M image/assets/lang/Francais.json => image/assets/lang/Francais.json +1 -2
@@ 648,10 648,9 @@
  "app_bell_settings_factory_reset": "Réinitialisation",
  "app_bell_settings_display_factory_reset_confirmation": "<text>Rétablir<br></br>la configuration ?</text>",
  "app_bell_meditation_timer": "Méditation",
  "app_bell_meditation_interval_chime": "gong intervalle",
  "app_bell_meditation_chime_interval": "gong intervalle",
  "app_bell_meditation_progress": "Méditation",
  "app_bell_meditation_interval_none": "Aucune",
  "app_bell_meditation_interval_every_x_minutes": "toutes les %0",
  "app_bell_meditation_put_down_and_wait": "<text>Posez Mudita Harmony<br>et attendez le gong</text>",
  "app_bell_meditation_thank_you_for_session": "<text>Merci<br>pour cette session</text>"
}

M image/assets/lang/Polski.json => image/assets/lang/Polski.json +7 -3
@@ 80,6 80,10 @@
  "common_minutes_lower": "minuty",
  "common_minutes_lower_genitive": "minut",
  "common_minute_short": "min",
  "common_second_lower": "sekunda",
  "common_seconds_lower": "sekundy",
  "common_seconds_lower_genitive": "sekund",
  "common_second_short": "sec",
  "common_paused": "Pauza",
  "common_text_copy": "Kopiuj tekst",
  "common_text_paste": "Wklej tekst",


@@ 691,10 695,10 @@
  "app_bell_settings_about_info_title": "Instrukcja i informacje dot. certyfikacji",
  "app_bell_settings_about_info_text": "www.mudita.com",
  "app_bell_meditation_timer": "Medytacja",
  "app_bell_meditation_interval_chime": "Dzwonek interwału",
  "app_bell_meditation_chime_interval": "Dzwonek interwału",
  "app_bell_meditation_progress": "Medytacja",
  "app_bell_meditation_interval_none": "Brak",
  "app_bell_meditation_interval_every_x_minutes": "co %0",
  "app_bell_meditation_put_down_and_wait": "<text>Odłóż Mudita Harmony<br>i czekaj na gong</text>",
  "app_bell_meditation_thank_you_for_session": "<text>Dziękujemy<br>za sesję</text>"
  "app_bell_meditation_thank_you_for_session": "<text>Dziękujemy<br>za sesję</text>",
  "app_bell_meditation_statistics": "Statystyki"
}

M module-apps/apps-common/CMakeLists.txt => module-apps/apps-common/CMakeLists.txt +0 -2
@@ 79,9 79,7 @@ target_sources(apps-common
        models/SongsRepository.hpp
        models/SongsModelInterface.hpp

        widgets/spinners/GenericSpinner.hpp
        widgets/spinners/ItemSpinner.hpp
        widgets/spinners/SpinnerPolicies.hpp
        widgets/spinners/Spinners.hpp
)


M module-apps/apps-common/widgets/TextSpinnerBox.cpp => module-apps/apps-common/widgets/TextSpinnerBox.cpp +7 -7
@@ 5,7 5,7 @@

namespace gui
{
    TextSpinnerBox::TextSpinnerBox(Item *parent, const std::vector<UTF8> &data, Boundaries boundaries) : HBox(parent)
    TextSpinnerBox::TextSpinnerBox(Item *parent, std::vector<UTF8> data, Boundaries boundaries) : HBox(parent)
    {
        setEdges(RectangleEdge::Bottom);



@@ 13,7 13,7 @@ namespace gui
        leftArrow->setMinimumSizeToFitImage();
        leftArrow->setVisible(false);

        spinner = new UTF8Spinner(data, Boundaries::Continuous, Orientation::Horizontal);
        spinner = new StringSpinner(std::move(data), boundaries, Orientation::Horizontal);
        spinner->setFont(style::window::font::medium);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        spinner->setEdges(RectangleEdge::All);


@@ 27,13 27,13 @@ namespace gui
        inputCallback = [&](Item &item, const InputEvent &event) { return spinner->onInput(event); };

        focusChangedCallback = [&](gui::Item &item) {
            if (focus && !spinner->isSingle()) {
            if (focus && spinner->size() != 1) {
                leftArrow->setVisible(true);
                rightArrow->setVisible(true);
                spinner->setFont(style::window::font::mediumbold);
                setPenWidth(style::window::default_border_focus_w);
            }
            else if (focus && spinner->isSingle()) {
            else if (focus && spinner->size() == 1) {
                leftArrow->setVisible(false);
                rightArrow->setVisible(false);
                spinner->setFont(style::window::font::mediumbold);


@@ 58,16 58,16 @@ namespace gui

    void TextSpinnerBox::setData(const std::vector<UTF8> &data)
    {
        spinner->setRange(data);
        spinner->set_range(data);
    }

    UTF8 TextSpinnerBox::getCurrentValue() const noexcept
    {
        return spinner->getCurrentValue();
        return spinner->value();
    }

    void TextSpinnerBox::setCurrentValue(UTF8 val)
    {
        spinner->setCurrentValue(val);
        spinner->set_value(val);
    }
} // namespace gui

M module-apps/apps-common/widgets/TextSpinnerBox.hpp => module-apps/apps-common/widgets/TextSpinnerBox.hpp +4 -4
@@ 14,14 14,14 @@ namespace gui
    class TextSpinnerBox : public HBox
    {
      public:
        TextSpinnerBox(Item *parent, const std::vector<UTF8> &data, Boundaries boundaries);
        TextSpinnerBox(Item *parent, std::vector<UTF8> data, Boundaries boundaries);
        void setData(const std::vector<UTF8> &data);
        [[nodiscard]] UTF8 getCurrentValue() const noexcept;
        void setCurrentValue(UTF8 val);

      private:
        UTF8Spinner *spinner = nullptr;
        ImageBox *leftArrow  = nullptr;
        ImageBox *rightArrow = nullptr;
        StringSpinner *spinner = nullptr;
        ImageBox *leftArrow    = nullptr;
        ImageBox *rightArrow   = nullptr;
    };
} // namespace gui

M module-apps/apps-common/widgets/TimeSetFmtSpinner.cpp => module-apps/apps-common/widgets/TimeSetFmtSpinner.cpp +6 -6
@@ 24,8 24,8 @@ namespace gui
        timeSetSpinner->setFont(focusFontName, noFocusFontName);
        timeSetSpinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));

        auto textRange = UTF8Spinner::Range{time::Locale::getAM(), time::Locale::getPM()};
        fmt            = new UTF8Spinner(textRange, Boundaries::Continuous);
        auto textRange = StringSpinner::range{time::Locale::getAM(), time::Locale::getPM()};
        fmt            = new StringSpinner(textRange, Boundaries::Continuous);
        updateFmtFont(noFocusFontName);
        fmt->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        fmt->setMargins(getFmtMargins(noFocusFontName));


@@ 74,10 74,10 @@ namespace gui
            if (timeFormat != newFormat) {
                timeSetSpinner->setHour(date::make12(hours).count());
                if (date::is_pm(hours)) {
                    fmt->setCurrentValue(time::Locale::getPM());
                    fmt->set_value(time::Locale::getPM());
                }
                else {
                    fmt->setCurrentValue(time::Locale::getAM());
                    fmt->set_value(time::Locale::getAM());
                }
            }



@@ 215,7 215,7 @@ namespace gui

    auto TimeSetFmtSpinner::isPM() const noexcept -> bool
    {
        return fmt->getCurrentValue() == utils::time::Locale::getPM().c_str();
        return fmt->value() == utils::time::Locale::getPM().c_str();
    }

    auto TimeSetFmtSpinner::getTimeFormat() const noexcept -> utils::time::Locale::TimeFormat


@@ 234,7 234,7 @@ namespace gui
            const auto hours   = std::chrono::hours{t->tm_hour};
            const auto time12H = date::make12(hours);
            const auto isPM    = date::is_pm(hours);
            fmt->setCurrentValue(isPM ? utils::time::Locale::getPM() : utils::time::Locale::getAM());
            fmt->set_value(isPM ? utils::time::Locale::getPM() : utils::time::Locale::getAM());
            timeSetSpinner->setTime(time12H.count(), t->tm_min);
        }
    }

M module-apps/apps-common/widgets/TimeSetFmtSpinner.hpp => module-apps/apps-common/widgets/TimeSetFmtSpinner.hpp +1 -1
@@ 76,7 76,7 @@ namespace gui
        void handleContentChanged() override;

        TimeSetSpinner *timeSetSpinner = nullptr;
        UTF8Spinner *fmt               = nullptr;
        StringSpinner *fmt             = nullptr;
        EditMode editMode              = EditMode::Edit;
        std::string focusFontName      = style::window::font::supersizeme;
        std::string noFocusFontName    = style::window::font::supersizemelight;

M module-apps/apps-common/widgets/TimeSetSpinner.cpp => module-apps/apps-common/widgets/TimeSetSpinner.cpp +10 -9
@@ 26,13 26,13 @@ namespace gui
        setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        setEdges(RectangleEdge::None);

        hour = new UIntegerSpinner(UIntegerSpinner::Range{hourMin, hourMax, hourStep}, Boundaries::Continuous);
        hour = new UIntegerSpinner(UIntegerSpinner::range{hourMin, hourMax, hourStep}, Boundaries::Continuous);
        updateFont(hour, noFocusFontName);

        hour->setAlignment(Alignment(Alignment::Horizontal::Right, Alignment::Vertical::Center));
        hour->setEdges(RectangleEdge::None);
        hour->setPenFocusWidth(style::time_set_spinner::focus::size);
        hour->setCurrentValue(0);
        hour->set_value(0);

        addWidget(hour);



@@ 42,14 42,14 @@ namespace gui
        colon->setEdges(RectangleEdge::None);
        colon->activeItem = false;

        minute = new UIntegerSpinnerFixed(UIntegerSpinnerFixed::Range{minuteMin, minuteMax, minuteStep},
        minute = new UIntegerSpinnerFixed(UIntegerSpinnerFixed::range{minuteMin, minuteMax, minuteStep},
                                          Boundaries::Continuous);
        updateFont(minute, noFocusFontName);
        minute->setPenFocusWidth(style::time_set_spinner::focus::size);

        minute->setAlignment(Alignment(Alignment::Horizontal::Left, Alignment::Vertical::Center));
        minute->setEdges(RectangleEdge::None);
        minute->setCurrentValue(0);
        minute->set_value(0);
        addWidget(minute);

        resizeItems();


@@ 133,12 133,12 @@ namespace gui

    auto TimeSetSpinner::setHour(int value) noexcept -> void
    {
        hour->setCurrentValue(value);
        hour->set_value(value);
    }

    auto TimeSetSpinner::setMinute(int value) noexcept -> void
    {
        minute->setCurrentValue(value);
        minute->set_value(value);
    }

    auto TimeSetSpinner::setFont(const std::string &newFontName) noexcept -> void


@@ 186,12 186,12 @@ namespace gui

    auto TimeSetSpinner::getHour() const noexcept -> int
    {
        return hour->getCurrentValue();
        return hour->value();
    }

    auto TimeSetSpinner::getMinute() const noexcept -> int
    {
        return minute->getCurrentValue();
        return minute->value();
    }

    void TimeSetSpinner::updateFocus(Item *newFocus)


@@ 220,7 220,8 @@ namespace gui

    auto TimeSetSpinner::setHourRange(std::uint32_t min, std::uint32_t max) -> void
    {
        hour->setRange(UIntegerSpinner::Range{min, max, hourStep});
        hour->set_range(
            UIntegerSpinner::range{static_cast<std::uint8_t>(min), static_cast<std::uint8_t>(max), hourStep});
    }

    auto TimeSetSpinner::getColonImage(const std::string &colonFont) const noexcept -> std::string

D module-apps/apps-common/widgets/spinners/GenericSpinner.hpp => module-apps/apps-common/widgets/spinners/GenericSpinner.hpp +0 -160
@@ 1,160 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "SpinnerPolicies.hpp"
#include <widgets/text/TextFixedSize.hpp>

namespace gui
{
    template <typename Policy> class GenericSpinner : public TextFixedSize
    {
      public:
        using Range = typename Policy::Range;
        using Type  = typename Policy::Type;

        using OnValueChanged = std::function<void(const Type &&)>;

        explicit GenericSpinner(Range range,
                                Boundaries boundaries   = Boundaries::Continuous,
                                Orientation orientation = Orientation::Vertical);

        [[nodiscard]] Type getCurrentValue() const noexcept;
        void setCurrentValue(Type val);
        void setRange(Range range);

        void setFocusEdges(RectangleEdge edges);
        bool onInput(const InputEvent &inputEvent) override;
        bool onFocus(bool state) override;
        [[nodiscard]] bool isSingle() const;
        [[nodiscard]] bool isAtMin() const;
        [[nodiscard]] bool isAtMax() const;

        OnValueChanged onValueChanged;

      private:
        void stepNext();
        void stepPrevious();
        bool isPreviousEvent(const InputEvent &inputEvent);
        bool isNextEvent(const InputEvent &inputEvent);
        void update();
        void invoke();

        Policy policy;
        RectangleEdge focusEdges = RectangleEdge::Bottom;
        Orientation orientation  = Orientation::Vertical;
    };

    template <typename ValuePolicy>
    GenericSpinner<ValuePolicy>::GenericSpinner(GenericSpinner::Range range,
                                                Boundaries boundaries,
                                                Orientation orientation)
        : policy{range, boundaries}, orientation(orientation)
    {
        setEditMode(EditMode::Browse);
        drawUnderline(false);
        update();
    }

    template <typename ValuePolicy> void GenericSpinner<ValuePolicy>::setFocusEdges(RectangleEdge edges)
    {
        focusEdges = edges;
    }

    template <typename Policy> void GenericSpinner<Policy>::setRange(Range range)
    {
        policy.updateRange(range);
        update();
    }

    template <typename ValuePolicy> void GenericSpinner<ValuePolicy>::setCurrentValue(const Type val)
    {
        policy.set(val);
        update();
    }

    template <typename Policy>
    typename GenericSpinner<Policy>::Type GenericSpinner<Policy>::getCurrentValue() const noexcept
    {
        return policy.get();
    }

    template <typename ValuePolicy> bool GenericSpinner<ValuePolicy>::isPreviousEvent(const InputEvent &inputEvent)
    {
        return (orientation == Orientation::Vertical && inputEvent.is(KeyCode::KEY_DOWN)) ||
               (orientation == Orientation::Horizontal && inputEvent.is(KeyCode::KEY_LEFT));
    }

    template <typename ValuePolicy> bool GenericSpinner<ValuePolicy>::isNextEvent(const InputEvent &inputEvent)
    {
        return (orientation == Orientation::Vertical && inputEvent.is(KeyCode::KEY_UP)) ||
               (orientation == Orientation::Horizontal && inputEvent.is(KeyCode::KEY_RIGHT));
    }

    template <typename ValuePolicy> bool GenericSpinner<ValuePolicy>::onInput(const InputEvent &inputEvent)
    {
        if (inputEvent.isShortRelease()) {
            if (isPreviousEvent(inputEvent)) {
                stepPrevious();
                return true;
            }
            else if (isNextEvent(inputEvent)) {
                stepNext();
                return true;
            }
        }
        return false;
    }

    template <typename ValuePolicy> bool GenericSpinner<ValuePolicy>::onFocus(bool state)
    {
        if (focus) {
            setEdges(focusEdges);
        }
        else {
            setEdges(RectangleEdge::None);
        }
        showCursor(state);
        return true;
    }

    template <typename Policy> void GenericSpinner<Policy>::stepNext()
    {
        if (policy.next()) {
            update();
            invoke();
        }
    }

    template <typename Policy> void GenericSpinner<Policy>::stepPrevious()
    {
        if (policy.previous()) {
            update();
            invoke();
        }
    }

    template <typename Policy> void GenericSpinner<Policy>::update()
    {
        setText(policy.str());
    }
    template <typename Policy> void GenericSpinner<Policy>::invoke()
    {
        if (onValueChanged) {
            onValueChanged(getCurrentValue());
        }
    }
    template <typename Policy> bool GenericSpinner<Policy>::isSingle() const
    {
        return policy.isSingle();
    }
    template <typename Policy> bool GenericSpinner<Policy>::isAtMin() const
    {
        return policy.isAtMin();
    }
    template <typename Policy> bool GenericSpinner<Policy>::isAtMax() const
    {
        return policy.isAtMax();
    }
} // namespace gui

M module-apps/apps-common/widgets/spinners/ItemSpinner.hpp => module-apps/apps-common/widgets/spinners/ItemSpinner.hpp +49 -38
@@ 3,27 3,27 @@

#pragma once

#include "SpinnerPolicies.hpp"
#include <Item.hpp>
#include <InputEvent.hpp>

namespace gui
{
    template <typename Policy> class ItemSpinner : public Item
    template <typename Container> class ItemSpinner : public Item
    {
      public:
        using Range = typename Policy::Range;
        using Type  = typename Policy::Type;
        using range      = typename Container::range;
        using value_type = typename Container::value_type;

        using OnValueChanged = std::function<void(const Type &&)>;
        using OnValueChanged = std::function<void(const value_type &&)>;

        explicit ItemSpinner(Item *parent,
                             Range range,
                             range range,
                             Boundaries boundaries   = Boundaries::Continuous,
                             Orientation orientation = Orientation::Vertical);

        [[nodiscard]] Type getCurrentValue() const noexcept;
        void setCurrentValue(Type val);
        void setRange(Range range);
        [[nodiscard]] value_type getCurrentValue() const noexcept;
        void setCurrentValue(value_type val);
        void setRange(range range);

        void setFocusEdges(RectangleEdge edges);
        bool onInput(const InputEvent &inputEvent) override;


@@ 33,6 33,16 @@ namespace gui
        OnValueChanged onValueChanged;

      private:
        inline typename Container::Boundaries convert_boundaries(gui::Boundaries boundaries)
        {
            switch (boundaries) {
            case gui::Boundaries::Fixed:
                return Container::Boundaries::Fixed;
            case gui::Boundaries::Continuous:
                return Container::Boundaries::Continuous;
            }
            return Container::Boundaries::Fixed;
        }
        void stepNext();
        void stepPrevious();
        bool isPreviousEvent(const InputEvent &inputEvent);


@@ 40,18 50,18 @@ namespace gui
        void update();
        void invoke();

        Policy policy;
        Container container;
        RectangleEdge focusEdges = RectangleEdge::Bottom;
        Orientation orientation  = Orientation::Vertical;
        gui::Item *currentLayout = nullptr;
    };

    template <typename ValuePolicy>
    ItemSpinner<ValuePolicy>::ItemSpinner(Item *parent,
                                          ItemSpinner::Range range,
                                          Boundaries boundaries,
                                          Orientation orientation)
        : Item(), policy{range, boundaries}, orientation(orientation)
    template <typename Container>
    ItemSpinner<Container>::ItemSpinner(Item *parent,
                                        ItemSpinner::range range,
                                        Boundaries boundaries,
                                        Orientation orientation)
        : Item(), container{range, convert_boundaries(boundaries)}, orientation(orientation)
    {
        this->parent = parent;
        if (parent != nullptr) {


@@ 59,41 69,42 @@ namespace gui
        }
    }

    template <typename ValuePolicy> void ItemSpinner<ValuePolicy>::setFocusEdges(RectangleEdge edges)
    template <typename Container> void ItemSpinner<Container>::setFocusEdges(RectangleEdge edges)
    {
        focusEdges = edges;
    }

    template <typename Policy> void ItemSpinner<Policy>::setRange(Range range)
    template <typename Container> void ItemSpinner<Container>::setRange(range range)
    {
        policy.updateRange(range);
        container.updateRange(range);
        update();
    }

    template <typename ValuePolicy> void ItemSpinner<ValuePolicy>::setCurrentValue(const Type val)
    template <typename Container> void ItemSpinner<Container>::setCurrentValue(const value_type val)
    {
        policy.set(val);
        container.set(val);
        update();
    }

    template <typename Policy> typename ItemSpinner<Policy>::Type ItemSpinner<Policy>::getCurrentValue() const noexcept
    template <typename Container>
    typename ItemSpinner<Container>::value_type ItemSpinner<Container>::getCurrentValue() const noexcept
    {
        return policy.get();
        return container.get();
    }

    template <typename ValuePolicy> bool ItemSpinner<ValuePolicy>::isPreviousEvent(const InputEvent &inputEvent)
    template <typename Container> bool ItemSpinner<Container>::isPreviousEvent(const InputEvent &inputEvent)
    {
        return (orientation == Orientation::Vertical && inputEvent.is(KeyCode::KEY_DOWN)) ||
               (orientation == Orientation::Horizontal && inputEvent.is(KeyCode::KEY_LEFT));
    }

    template <typename ValuePolicy> bool ItemSpinner<ValuePolicy>::isNextEvent(const InputEvent &inputEvent)
    template <typename Container> bool ItemSpinner<Container>::isNextEvent(const InputEvent &inputEvent)
    {
        return (orientation == Orientation::Vertical && inputEvent.is(KeyCode::KEY_UP)) ||
               (orientation == Orientation::Horizontal && inputEvent.is(KeyCode::KEY_RIGHT));
    }

    template <typename ValuePolicy> bool ItemSpinner<ValuePolicy>::onInput(const InputEvent &inputEvent)
    template <typename Container> bool ItemSpinner<Container>::onInput(const InputEvent &inputEvent)
    {
        if (inputEvent.isShortRelease()) {
            if (isPreviousEvent(inputEvent)) {


@@ 108,43 119,43 @@ namespace gui
        return false;
    }

    template <typename Policy> void ItemSpinner<Policy>::stepNext()
    template <typename Container> void ItemSpinner<Container>::stepNext()
    {
        if (policy.next()) {
        if (container.next()) {
            update();
            invoke();
        }
    }

    template <typename Policy> void ItemSpinner<Policy>::stepPrevious()
    template <typename Container> void ItemSpinner<Container>::stepPrevious()
    {
        if (policy.previous()) {
        if (container.previous()) {
            update();
            invoke();
        }
    }

    template <typename Policy> void ItemSpinner<Policy>::update()
    template <typename Container> void ItemSpinner<Container>::update()
    {
        if (currentLayout) {
        if (currentLayout != nullptr) {
            this->removeWidget(currentLayout);
        }
        currentLayout = policy.get();
        currentLayout = container.get();
        this->addWidget(currentLayout);
        informContentChanged();
    }
    template <typename Policy> void ItemSpinner<Policy>::invoke()
    template <typename Container> void ItemSpinner<Container>::invoke()
    {
        if (onValueChanged) {
            onValueChanged(getCurrentValue());
        }
    }
    template <typename Policy> bool ItemSpinner<Policy>::isAtMin() const
    template <typename Container> bool ItemSpinner<Container>::isAtMin() const
    {
        return policy.isAtMin();
        return container.is_min();
    }
    template <typename Policy> bool ItemSpinner<Policy>::isAtMax() const
    template <typename Container> bool ItemSpinner<Container>::isAtMax() const
    {
        return policy.isAtMax();
        return container.is_max();
    }
} // namespace gui

A module-apps/apps-common/widgets/spinners/Model.hpp => module-apps/apps-common/widgets/spinners/Model.hpp +212 -0
@@ 0,0 1,212 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <vector>
#include <string>
#include <type_traits>

/// Generic elements container.
/// By default, the container will choose the most optimal way of elements storage.
/// Fundamental types(ints, float, doubles, chars) are stored as simple variables. Compound types are stored in
/// std::vector.
/// Sometimes it is necessary to force the latter way of storing values even for fundamental types. For
/// instance, the user might want to store a fixed list of integers to iterate. In that case, set the force parameter to
/// true.
template <class ElementType, bool force = false, typename = void> class Model
{
  public:
    enum class Boundaries
    {
        Fixed,     /// Stop scrolling upon reaching last/first element
        Continuous /// Jump to beginning/end upon reaching last/first element
    };
    using range      = std::vector<ElementType>;
    using value_type = ElementType;

    Model(range &&r, Boundaries boundaries) : elements{std::move(r)}, it{elements.begin()}, boundaries{boundaries}
    {}

    Model(const range &r, Boundaries boundaries) : elements{r}, it{elements.begin()}, boundaries{boundaries}
    {}

    ElementType get() const
    {
        return it == elements.end() ? ElementType{} : *it;
    }

    void set(ElementType val)
    {
        const auto e = std::find_if(elements.begin(), elements.end(), [&val](const auto &i) { return i == val; });
        if (e != elements.end()) {
            it = e;
        }
    }

    bool next()
    {
        bool ret{true};
        if (std::next(it) == elements.end()) {
            if (boundaries == Boundaries::Continuous) {
                it = elements.begin();
            }
            else {
                ret = false;
            }
        }
        else {
            it = std::next(it);
        }
        return ret;
    }

    bool previous()
    {
        bool ret{true};
        if (it == elements.begin()) {
            if (boundaries == Boundaries::Continuous) {
                it = std::prev(elements.end());
            }
            else {
                ret = false;
            }
        }
        else {
            it = std::prev(it);
        }
        return ret;
    }

    void set_range(range newRange)
    {
        if (elements != newRange) {
            elements = newRange;
            it       = elements.begin();
        }
    }

    [[nodiscard]] size_t size() const
    {
        return elements.size();
    }

    [[nodiscard]] bool is_min() const
    {
        return it == elements.begin();
    }

    [[nodiscard]] bool is_max() const
    {
        return std::next(it) == elements.end();
    }

  private:
    range elements;
    typename range::iterator it = elements.end();
    const Boundaries boundaries{};
};

template <typename ElementType, bool force>
class Model<ElementType, force, std::enable_if_t<std::is_fundamental_v<ElementType> and not force>>
{
    struct details
    {
        struct range
        {
            ElementType min{};
            ElementType max{};
            ElementType step{};

            bool operator!=(const range &oth) const
            {
                return min != oth.min || max != oth.max || step != oth.step;
            }
        };
    };

  public:
    enum class Boundaries
    {
        Fixed,
        Continuous
    };
    using range      = typename details::range;
    using value_type = ElementType;

    Model(range &&elements, Boundaries boundaries)
        : elements{std::move(elements)}, value{elements.min}, boundaries{boundaries}
    {}

    Model(range &elements, Boundaries boundaries) : elements{elements}, value{elements.min}, boundaries{boundaries}
    {}

    ElementType get() const
    {
        return value;
    }

    void set(ElementType val)
    {
        value = val;
    }

    bool next()
    {
        bool ret{true};
        if (value >= elements.max) {
            if (boundaries == Boundaries::Continuous) {
                value = elements.min;
            }
            else {
                value = elements.max;
                ret   = false;
            }
        }
        else {
            value += elements.step;
        }
        return ret;
    }

    bool previous()
    {
        bool ret{true};
        if (value <= elements.min) {
            if (boundaries == Boundaries::Continuous) {
                value = elements.max;
            }
            else {
                value = elements.min;
                ret   = false;
            }
        }
        else {
            value -= elements.step;
        }
        return ret;
    }

    void set_range(range newRange)
    {
        if (elements != newRange) {
            elements = newRange;
            value    = elements.min;
        }
    }

    [[nodiscard]] bool is_min() const
    {
        return value == elements.min;
    }

    [[nodiscard]] bool is_max() const
    {
        return value == elements.max;
    }

  private:
    range elements;
    ElementType value{};
    const Boundaries boundaries{};
};

D module-apps/apps-common/widgets/spinners/SpinnerContents.hpp => module-apps/apps-common/widgets/spinners/SpinnerContents.hpp +0 -58
@@ 1,58 0,0 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <optional>

namespace gui
{
    template <typename ValueT, typename StringT> class NumWithString
    {
      public:
        NumWithString() = default;

        NumWithString(ValueT value, StringT string) : value{value}, string{string}
        {}

        explicit NumWithString(StringT string) : string{string}
        {}
        explicit NumWithString(ValueT value) : value{value}
        {}

        bool operator==(const NumWithString &oth) const
        {
            return oth.toStr() == toStr();
        }

        operator StringT() const
        {
            return toStr();
        }

        std::optional<ValueT> getValue() const
        {
            return value;
        }

        std::optional<StringT> getSuffix() const
        {
            return string;
        }

      private:
        StringT toStr() const
        {
            StringT retStr;
            if (value) {
                retStr += std::to_string(*value) + " ";
            }
            if (string) {
                retStr += *string;
            }
            return retStr;
        }
        std::optional<ValueT> value;
        std::optional<StringT> string;
    };
} // namespace gui

D module-apps/apps-common/widgets/spinners/SpinnerPolicies.hpp => module-apps/apps-common/widgets/spinners/SpinnerPolicies.hpp +0 -410
@@ 1,410 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <gui/Common.hpp>

#include <vector>
#include <cstdint>
#include <iomanip>
#include <utf8/UTF8.hpp>

namespace gui
{
    template <typename ValType> class StringPolicy
    {
      public:
        using Range = std::vector<ValType>;
        using Type  = ValType;

        StringPolicy(Range range, Boundaries boundaries) : range{range}, boundaries{boundaries}
        {}

        ValType get() const
        {
            return pos < range.size() ? range[pos] : ValType{};
        }

        UTF8 str() const
        {
            return pos < range.size() ? range[pos] : UTF8{};
        }

        void set(ValType val)
        {
            for (auto i = 0U; i < range.size(); i++) {
                if (range[i] == val) {
                    pos = i;
                    break;
                }
            }
        }

        bool next()
        {
            bool ret{true};
            if (pos >= upRange()) {
                if (boundaries == Boundaries::Continuous) {
                    pos = 0;
                }
                else {
                    pos = upRange();
                    ret = false;
                }
            }
            else {
                pos++;
            }
            return ret;
        }

        bool previous()
        {
            bool ret{true};
            if (pos <= 0) {
                if (boundaries == Boundaries::Continuous) {
                    pos = upRange();
                }
                else {
                    pos = 0;
                    ret = false;
                }
            }
            else {
                pos--;
            }
            return ret;
        }

        void updateRange(Range newRange)
        {
            if (range != newRange) {
                range = newRange;
                pos   = 0;
            }
        }

        [[nodiscard]] bool isSingle() const
        {
            return range.size() == 1;
        }
        [[nodiscard]] bool isAtMin() const
        {
            return pos == 0;
        }
        [[nodiscard]] bool isAtMax() const
        {
            return pos == upRange();
        }

      private:
        std::uint32_t upRange() const
        {
            return range.size() - 1;
        }

        Range range;
        std::uint32_t pos{};
        const Boundaries boundaries{};
    };

    template <typename ValType> class WidgetPolicy
    {
      public:
        using Range = std::vector<ValType>;
        using Type  = ValType;

        WidgetPolicy(Range range, Boundaries boundaries) : range{range}, boundaries{boundaries}
        {}

        ValType get() const
        {
            return pos < range.size() ? range[pos] : ValType{};
        }

        void set(ValType val)
        {
            for (auto i = 0U; i < range.size(); i++) {
                if (range[i] == val) {
                    pos = i;
                    break;
                }
            }
        }

        bool next()
        {
            bool ret{true};
            if (pos >= upRange()) {
                if (boundaries == Boundaries::Continuous) {
                    pos = 0;
                }
                else {
                    pos = upRange();
                    ret = false;
                }
            }
            else {
                pos++;
            }
            return ret;
        }

        bool previous()
        {
            bool ret{true};
            if (pos <= 0) {
                if (boundaries == Boundaries::Continuous) {
                    pos = upRange();
                }
                else {
                    pos = 0;
                    ret = false;
                }
            }
            else {
                pos--;
            }
            return ret;
        }

        void updateRange(Range newRange)
        {
            if (range != newRange) {
                range = newRange;
                pos   = 0;
            }
        }

        [[nodiscard]] bool isAtMin() const
        {
            return pos == 0;
        }
        [[nodiscard]] bool isAtMax() const
        {
            return pos == upRange();
        }

      private:
        std::uint32_t upRange() const
        {
            return range.size() - 1;
        }

        Range range;
        std::uint32_t pos{};
        const Boundaries boundaries{};
    };

    template <typename ValType> class DefaultNumericFormatter
    {
      public:
        std::string operator()(const ValType val) const
        {
            return std::to_string(val);
        }
    };

    template <typename ValType, int Width> class FixedSizeFormatter
    {
      public:
        std::string operator()(const ValType val) const
        {
            std::stringstream outStream;
            outStream << std::setw(Width) << std::setfill('0') << val;
            return outStream.str();
        }
    };

    template <typename ValType, typename Formatter = DefaultNumericFormatter<ValType>> class NumericPolicy
    {
        struct RangeImpl
        {
            ValType min;
            ValType max;
            ValType step;
            bool operator!=(const RangeImpl &oth) const
            {
                return min != oth.min || max != oth.max || step != oth.step;
            }
        };

      public:
        using Range = RangeImpl;
        using Type  = ValType;

        NumericPolicy(Range range, Boundaries boundaries) : range{range}, boundaries{boundaries}
        {}

        ValType get() const
        {
            return currentValue;
        }

        UTF8 str() const
        {
            return Formatter{}(currentValue);
        }

        void set(ValType val)
        {
            currentValue = val;
        }

        bool next()
        {
            bool ret{true};
            if (currentValue >= range.max) {
                if (boundaries == Boundaries::Continuous) {
                    currentValue = range.min;
                }
                else {
                    currentValue = range.max;
                    ret          = false;
                }
            }
            else {
                currentValue += range.step;
            }
            return ret;
        }

        bool previous()
        {
            bool ret{true};
            if (currentValue <= range.min) {
                if (boundaries == Boundaries::Continuous) {
                    currentValue = range.max;
                }
                else {
                    currentValue = range.min;
                    ret          = false;
                }
            }
            else {
                currentValue -= range.step;
            }
            return ret;
        }

        void updateRange(Range newRange)
        {
            if (range != newRange) {
                range        = newRange;
                currentValue = 0;
            }
        }

        [[nodiscard]] bool isAtMin() const
        {
            return currentValue == range.min;
        }
        [[nodiscard]] bool isAtMax() const
        {
            return currentValue == range.max;
        }

      private:
        Range range;
        ValType currentValue{};
        const Boundaries boundaries{};
    };

    template <typename ValType, typename RepresentationType = UTF8> class ModelDelegatePolicy
    {
      public:
        using Range = std::vector<ValType>;
        using Type  = ValType;

        ModelDelegatePolicy(Range range, Boundaries boundaries) : range{range}, boundaries{boundaries}
        {}

        Type get() const
        {
            return pos < range.size() ? range[pos] : Type{};
        }

        UTF8 str() const
        {
            return pos < range.size() ? RepresentationType{range[pos]} : RepresentationType{};
        }

        void set(ValType val)
        {
            for (auto i = 0U; i < range.size(); i++) {
                if (range[i] == val) {
                    pos = i;
                    break;
                }
            }
        }

        bool next()
        {
            bool ret{true};
            if (pos >= upRange()) {
                if (boundaries == Boundaries::Continuous) {
                    pos = 0;
                }
                else {
                    pos = upRange();
                    ret = false;
                }
            }
            else {
                pos++;
            }
            return ret;
        }

        bool previous()
        {
            bool ret{true};
            if (pos <= 0) {
                if (boundaries == Boundaries::Continuous) {
                    pos = upRange();
                }
                else {
                    pos = 0;
                    ret = false;
                }
            }
            else {
                pos--;
            }
            return ret;
        }

        void updateRange(Range newRange)
        {
            if (range != newRange) {
                range = newRange;
                pos   = 0;
            }
        }
        [[nodiscard]] bool isSingle() const
        {
            return range.size() == 1;
        }
        [[nodiscard]] bool isAtMin() const
        {
            return pos == 0;
        }
        [[nodiscard]] bool isAtMax() const
        {
            return pos == upRange();
        }

      private:
        std::uint32_t upRange() const
        {
            return range.size() - 1;
        }

        Range range;
        std::uint32_t pos{};
        const Boundaries boundaries{};
    };

} // namespace gui

M module-apps/apps-common/widgets/spinners/Spinners.hpp => module-apps/apps-common/widgets/spinners/Spinners.hpp +22 -7
@@ 3,16 3,31 @@

#pragma once

#include "GenericSpinner.hpp"
#include "ItemSpinner.hpp"

#include "Model.hpp"
#include "StringOutputSpinner.hpp"
#include "ItemSpinner.hpp"

namespace gui
{
    using UTF8Spinner          = GenericSpinner<StringPolicy<UTF8>>;
    using UIntegerSpinner      = GenericSpinner<NumericPolicy<std::uint32_t>>;
    using UIntegerSpinnerFixed = GenericSpinner<NumericPolicy<std::uint32_t, FixedSizeFormatter<std::uint32_t, 2>>>;
    using IntegerSpinner       = GenericSpinner<NumericPolicy<std::int32_t>>;
    using WidgetSpinner        = ItemSpinner<WidgetPolicy<Item *>>;
    template <typename ValType, size_t Width> struct FixedIntegerFormatter
    {
        std::string operator()(const ValType val) const
        {
            std::stringstream outStream;
            outStream << std::setw(Width) << std::setfill('0') << val;
            return outStream.str();
        }
    };

    using StringContainer = Model<UTF8>;
    using UINT8Container  = Model<std::uint8_t>;

    using StringSpinner        = StringOutputSpinner<StringContainer>;
    using UIntegerSpinner      = StringOutputSpinner<UINT8Container>;
    using UIntegerSpinnerFixed = StringOutputSpinner<UINT8Container, FixedIntegerFormatter<std::uint32_t, 2>>;
    using WidgetSpinner        = ItemSpinner<Model<Item *>>;
    template <typename T> using UIntegerSpinnerWithFormatter = StringOutputSpinner<UINT8Container, T>;

    template <typename ModelType> using ModelDelegateSpinner = GenericSpinner<ModelDelegatePolicy<ModelType>>;
} // namespace gui

A module-apps/apps-common/widgets/spinners/StringOutputSpinner.hpp => module-apps/apps-common/widgets/spinners/StringOutputSpinner.hpp +237 -0
@@ 0,0 1,237 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <widgets/text/TextFixedSize.hpp>

#include <string>
#include <type_traits>

namespace details
{
    template <typename T, typename = void> struct container_data
    {
        using value = T;
    };

    template <typename T> struct container_data<T, std::enable_if_t<not std::is_fundamental_v<T>>>
    {
        using value = typename T::value_type;
    };

    template <class T> using container_data_v = typename container_data<T>::value;

} // namespace details

namespace gui
{
    // This spinner operates on container elements and transforms the current container into a string.
    // For the containers of types that are convertible to strings like std::string, integer, floating-point integers,
    // there is no need to provide the custom formatter. However, it is possible to pass a formatter if a user wants to
    // perform custom formatting. It is not possible to use a formatter when using container of std::string.
    template <typename Container, typename Formatter = void> class StringOutputSpinner : public TextFixedSize
    {
      public:
        using range      = typename Container::range;
        using value_type = details::container_data_v<Container>;

        explicit StringOutputSpinner(Container &&container, Orientation orientation = Orientation::Vertical)
            : container{std::move(container)}, orientation{orientation}
        {
            init();
        }

        explicit StringOutputSpinner(range &range,
                                     Boundaries boundaries   = Boundaries::Fixed,
                                     Orientation orientation = Orientation::Vertical)
            : container{range, convert_boundaries(boundaries)}, orientation{orientation}
        {
            init();
        }

        explicit StringOutputSpinner(range &&range,
                                     Boundaries boundaries   = Boundaries::Fixed,
                                     Orientation orientation = Orientation::Vertical)
            : container{std::move(range), convert_boundaries(boundaries)}, orientation{orientation}
        {
            init();
        }

        using OnValueChanged = std::function<void(const value_type &)>;
        void setFocusEdges(RectangleEdge edges);
        bool onInput(const InputEvent &inputEvent) override;
        bool onFocus(bool state) override;
        [[nodiscard]] size_t size() const;
        [[nodiscard]] bool is_min() const;
        [[nodiscard]] bool is_max() const;

        OnValueChanged onValueChanged;

        void set_range(const range &range);
        void set_value(const value_type &value);
        [[nodiscard]] auto value() const noexcept;

      private:
        inline typename Container::Boundaries convert_boundaries(gui::Boundaries boundaries)
        {
            switch (boundaries) {
            case gui::Boundaries::Fixed:
                return Container::Boundaries::Fixed;
            case gui::Boundaries::Continuous:
                return Container::Boundaries::Continuous;
            }
            return Container::Boundaries::Fixed;
        }
        void init();
        [[nodiscard]] std::string value_as_str() const noexcept;
        void next();
        void previous();
        bool is_previous_event(const InputEvent &inputEvent);
        bool is_next_event(const InputEvent &inputEvent);
        void update();
        void invoke();

        static constexpr bool is_string =
            std::is_same_v<value_type, std::string> or std::is_convertible_v<value_type, std::string>;
        static constexpr bool is_fundamental       = std::is_fundamental_v<value_type>;
        static constexpr bool is_formatter_defined = not std::is_same_v<Formatter, void>;

        Container container;
        RectangleEdge focusEdges = RectangleEdge::Bottom;
        Orientation orientation  = Orientation::Vertical;
    };

    template <typename Container, typename Formatter>
    auto StringOutputSpinner<Container, Formatter>::value() const noexcept
    {
        return container.get();
    }

    template <typename Container, typename Formatter>
    std::string StringOutputSpinner<Container, Formatter>::value_as_str() const noexcept
    {
        if constexpr (is_string) {
            return container.get();
        }
        else if constexpr (is_fundamental and not is_formatter_defined) {
            return std::to_string(container.get());
        }
        else {
            return Formatter{}(container.get());
        }
    }

    template <typename Container, typename Formatter>
    void StringOutputSpinner<Container, Formatter>::setFocusEdges(RectangleEdge edges)
    {
        focusEdges = edges;
    }

    template <typename Container, typename Formatter>
    bool StringOutputSpinner<Container, Formatter>::onInput(const InputEvent &inputEvent)
    {
        if (inputEvent.isShortRelease()) {
            if (is_previous_event(inputEvent)) {
                previous();
                return true;
            }
            else if (is_next_event(inputEvent)) {
                next();
                return true;
            }
        }
        return false;
    }

    template <typename Container, typename Formatter>
    bool StringOutputSpinner<Container, Formatter>::onFocus(bool state)
    {
        if (focus) {
            setEdges(focusEdges);
        }
        else {
            setEdges(RectangleEdge::None);
        }
        showCursor(state);
        return true;
    }

    template <typename Container, typename Formatter> size_t StringOutputSpinner<Container, Formatter>::size() const
    {
        return container.size();
    }

    template <typename Container, typename Formatter> bool StringOutputSpinner<Container, Formatter>::is_min() const
    {
        return container.is_min();
    }

    template <typename Container, typename Formatter> bool StringOutputSpinner<Container, Formatter>::is_max() const
    {
        return container.is_max();
    }

    template <typename Container, typename Formatter>
    void StringOutputSpinner<Container, Formatter>::set_value(const value_type &value)
    {
        container.set(value);
        update();
    }

    template <typename Container, typename Formatter>
    void StringOutputSpinner<Container, Formatter>::set_range(const range &range)
    {
        container.set_range(range);
        update();
    }

    template <typename Container, typename Formatter> void StringOutputSpinner<Container, Formatter>::next()
    {
        if (container.next()) {
            update();
            invoke();
        }
    }

    template <typename Container, typename Formatter> void StringOutputSpinner<Container, Formatter>::previous()
    {
        if (container.previous()) {
            update();
            invoke();
        }
    }
    template <typename Container, typename Formatter> void StringOutputSpinner<Container, Formatter>::update()
    {
        setText(value_as_str());
    }

    template <typename Container, typename Formatter> void StringOutputSpinner<Container, Formatter>::invoke()
    {
        if (onValueChanged) {
            onValueChanged(value());
        }
    }

    template <typename Container, typename Formatter>
    bool StringOutputSpinner<Container, Formatter>::is_previous_event(const InputEvent &inputEvent)
    {
        return (orientation == Orientation::Vertical && inputEvent.is(KeyCode::KEY_DOWN)) ||
               (orientation == Orientation::Horizontal && inputEvent.is(KeyCode::KEY_LEFT));
    }

    template <typename Container, typename Formatter>
    bool StringOutputSpinner<Container, Formatter>::is_next_event(const InputEvent &inputEvent)
    {
        return (orientation == Orientation::Vertical && inputEvent.is(KeyCode::KEY_UP)) ||
               (orientation == Orientation::Horizontal && inputEvent.is(KeyCode::KEY_RIGHT));
    }

    template <typename Container, typename Formatter> void StringOutputSpinner<Container, Formatter>::init()
    {
        setEditMode(EditMode::Browse);
        drawUnderline(false);
        update();
    }

} // namespace gui

M module-apps/tests/CMakeLists.txt => module-apps/tests/CMakeLists.txt +1 -0
@@ 5,6 5,7 @@ add_catch2_executable(
        test-CallbackStorage.cpp
        test-PhoneModesPolicies.cpp
        tests-BluetoothSettingsModel.cpp
        test-Model.cpp
    LIBS
        module-apps
)

A module-apps/tests/test-Model.cpp => module-apps/tests/test-Model.cpp +199 -0
@@ 0,0 1,199 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <catch2/catch.hpp>

#include <apps-common/widgets/spinners/Model.hpp>

TEST_CASE("Model")
{
    SECTION("Empty")
    {
        using UINT8Model = Model<std::uint8_t>;

        auto container = UINT8Model{{}, UINT8Model::Boundaries::Fixed};

        REQUIRE(container.get() == 0);
    }

    SECTION("Force underlying structure")
    {
        using ForcedContainer = Model<std::uint8_t, true>;

        auto container = ForcedContainer{{5, 10, 2, 30, 25}, ForcedContainer::Boundaries::Fixed};

        REQUIRE(container.get() == 5);
        container.set(25);
        REQUIRE(container.get() == 25);
        container.set(2);
        REQUIRE(container.get() == 2);
    }

    SECTION("Fundamental types")
    {
        using UINT8Model      = Model<std::uint8_t>;
        auto range            = UINT8Model::range{0, 10, 1};
        const auto boundaries = UINT8Model::Boundaries::Fixed;
        SECTION("set")
        {
            auto container = UINT8Model{range, boundaries};

            REQUIRE(container.get() == 0);
            container.set(5);
            REQUIRE(container.get() == 5);
        }

        SECTION("check step")
        {
            auto container = UINT8Model{UINT8Model::range{0, 6, 3}, boundaries};

            REQUIRE(container.get() == 0);
            container.next();
            REQUIRE(container.get() == 3);
            container.previous();
            REQUIRE(container.get() == 0);
        }

        SECTION("Check boundaries")
        {
            SECTION("Fixed")
            {
                auto container = UINT8Model{range, boundaries};

                REQUIRE(container.get() == 0);
                container.previous();
                REQUIRE(container.get() == 0);

                container.set(10);
                REQUIRE(container.get() == 10);
                container.next();
                REQUIRE(container.get() == 10);
            }

            SECTION("Continuous")
            {
                auto container = UINT8Model{range, UINT8Model::Boundaries::Continuous};

                REQUIRE(container.get() == 0);
                container.previous();
                REQUIRE(container.get() == 10);

                container.set(10);
                REQUIRE(container.get() == 10);
                container.next();
                REQUIRE(container.get() == 0);
            }
        }

        SECTION("is_min/is_max")
        {
            auto container = UINT8Model{range, boundaries};

            REQUIRE(container.is_min());
            REQUIRE_FALSE(container.is_max());

            container.next();
            REQUIRE_FALSE(container.is_min());
            REQUIRE_FALSE(container.is_max());

            container.set(10);
            REQUIRE_FALSE(container.is_min());
            REQUIRE(container.is_max());
        }

        SECTION("set_range")
        {
            auto container = UINT8Model{range, boundaries};

            REQUIRE(container.get() == 0);

            container.set_range(UINT8Model::range{1, 3, 1});
            REQUIRE(container.get() == 1);
        }
    }

    SECTION("Complex types")
    {
        using ComplexContainer = Model<std::string>;
        const auto one         = "one";
        const auto two         = "two";
        const auto three       = "three";
        auto range             = ComplexContainer::range{one, two, three};
        const auto boundaries  = ComplexContainer::Boundaries::Fixed;
        SECTION("set")
        {

            auto container = ComplexContainer{range, boundaries};

            REQUIRE(container.get() == one);
            container.set(two);
            REQUIRE(container.get() == two);
        }

        SECTION("check step")
        {
            auto container = ComplexContainer{range, boundaries};

            REQUIRE(container.get() == one);
            container.next();
            REQUIRE(container.get() == two);
        }

        SECTION("Check boundaries")
        {
            SECTION("Fixed")
            {
                auto container = ComplexContainer{range, boundaries};

                REQUIRE(container.get() == one);
                container.previous();
                REQUIRE(container.get() == one);

                container.set(three);
                REQUIRE(container.get() == three);
                container.next();
                REQUIRE(container.get() == three);
            }

            SECTION("Continuous")
            {
                auto container = ComplexContainer{range, ComplexContainer::Boundaries::Continuous};

                REQUIRE(container.get() == one);
                container.previous();
                REQUIRE(container.get() == three);

                container.set(three);
                REQUIRE(container.get() == three);
                container.next();
                REQUIRE(container.get() == one);
            }
        }

        SECTION("is_min/is_max")
        {
            auto container = ComplexContainer{range, boundaries};

            REQUIRE(container.is_min());
            REQUIRE_FALSE(container.is_max());

            container.next();
            REQUIRE_FALSE(container.is_min());
            REQUIRE_FALSE(container.is_max());

            container.set(three);
            REQUIRE_FALSE(container.is_min());
            REQUIRE(container.is_max());
        }

        SECTION("set_range")
        {
            auto container = ComplexContainer{range, boundaries};

            REQUIRE(container.get() == one);

            container.set_range(ComplexContainer::range{"two", "three", "four"});
            REQUIRE(container.get() == two);
        }
    }
}

M module-bsp/board/rt1051/bsp/audio/CodecMAX98090.hpp => module-bsp/board/rt1051/bsp/audio/CodecMAX98090.hpp +4 -2
@@ 8,6 8,8 @@
#include "drivers/i2c/DriverI2C.hpp"
#include <cstdint>

#include <cstdint>

class CodecParamsMAX98090 : public CodecParams
{
  public:


@@ 66,8 68,8 @@ class CodecParamsMAX98090 : public CodecParams
    bool micBiasEnable             = false;
    std::uint8_t playbackPathGain  = 0;
    std::uint8_t playbackPathAtten = 0;
    InputPath inputPath   = InputPath::None;
    OutputPath outputPath = OutputPath::None;
    InputPath inputPath            = InputPath::None;
    OutputPath outputPath          = OutputPath::None;
};

class CodecMAX98090 : public Codec

M products/BellHybrid/BellHybridMain.cpp => products/BellHybrid/BellHybridMain.cpp +2 -3
@@ 9,7 9,7 @@
#include <application-bell-background-sounds/ApplicationBellBackgroundSounds.hpp>
#include <application-bell-bedtime/ApplicationBellBedtime.hpp>
#include <application-bell-main/ApplicationBellMain.hpp>
#include <application-bell-meditation-timer/ApplicationBellMeditationTimer.hpp>
#include <application-bell-meditation-timer/MeditationTimer.hpp>
#include <application-bell-settings/ApplicationBellSettings.hpp>
#include <application-bell-powernap/ApplicationBellPowerNap.hpp>



@@ 113,8 113,7 @@ int main()
                app::CreateLauncher<app::ApplicationBellOnBoarding>(app::applicationBellOnBoardingName));
            applications.push_back(
                app::CreateLauncher<app::ApplicationBellBackgroundSounds>(app::applicationBellBackgroundSoundsName));
            applications.push_back(
                app::CreateLauncher<app::ApplicationBellMeditationTimer>(app::applicationBellMeditationTimerName));
            applications.push_back(app::CreateLauncher<app::MeditationTimer>(app::MeditationTimer::defaultName));
            // start application manager
            return sysmgr->RunSystemService(
                std::make_shared<app::manager::ApplicationManager>(

M products/BellHybrid/apps/application-bell-background-sounds/windows/BGSoundsTimerSelectWindow.cpp => products/BellHybrid/apps/application-bell-background-sounds/windows/BGSoundsTimerSelectWindow.cpp +6 -6
@@ 86,20 86,20 @@ namespace gui
    {
        auto range = presenter->getTimerValuesRange();

        spinner = new UTF8Spinner(toUTF8Range(range), Boundaries::Fixed);
        spinner = new StringSpinner(toUTF8Range(range), Boundaries::Fixed);
        spinner->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::h);
        spinner->setFont(bgSoundsStyle::timerValueFont);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        spinner->setEdges(RectangleEdge::None);
        spinner->setFocusEdges(RectangleEdge::None);
        auto currentValue = timerValueToUTF8(presenter->getCurrentTimerValue());
        spinner->setCurrentValue(std::move(currentValue));
        spinner->set_value(std::move(currentValue));
        spinner->onValueChanged = [this](const auto &) {
            body->setMinMaxArrowsVisibility(spinner->isAtMin(), spinner->isAtMax());
            body->setMinMaxArrowsVisibility(spinner->is_min(), spinner->is_max());
            updateBottomDescription();
        };
        body->getCenterBox()->addWidget(spinner);
        body->setMinMaxArrowsVisibility(spinner->isAtMin(), spinner->isAtMax());
        body->setMinMaxArrowsVisibility(spinner->is_min(), spinner->is_max());
    }

    void BGSoundsTimerSelectWindow::createBottomDescription()


@@ 117,7 117,7 @@ namespace gui
    }
    void BGSoundsTimerSelectWindow::updateBottomDescription()
    {
        const auto currentVal = spinner->getCurrentValue();
        const auto currentVal = spinner->value();
        bottomDescription->setText(utils::language::getCorrectMinutesNumeralForm(UTF8ToTimerValue(currentVal).count()));

        const bool isDescriptionVisible = UTF8ToTimerValue(currentVal) != offValue;


@@ 158,7 158,7 @@ namespace gui
            return true;
        }
        if (inputEvent.isShortRelease(KeyCode::KEY_ENTER)) {
            auto currentValue = UTF8ToTimerValue(spinner->getCurrentValue());
            auto currentValue = UTF8ToTimerValue(spinner->value());
            presenter->setTimerValue(currentValue);

            auto audioSwitchData                        = std::make_unique<BGSoundsSwitchData>(std::move(audioContext));

M products/BellHybrid/apps/application-bell-background-sounds/windows/BGSoundsTimerSelectWindow.hpp => products/BellHybrid/apps/application-bell-background-sounds/windows/BGSoundsTimerSelectWindow.hpp +1 -1
@@ 18,7 18,7 @@ namespace gui
    {
        std::unique_ptr<app::bgSounds::BGSoundsTimerSelectContract::Presenter> presenter;
        BellBaseLayout *body    = nullptr;
        UTF8Spinner *spinner    = nullptr;
        StringSpinner *spinner  = nullptr;
        Text *bottomDescription = nullptr;
        std::unique_ptr<BGSoundsAudioContext> audioContext;


M products/BellHybrid/apps/application-bell-background-sounds/windows/BGSoundsVolumeWindow.cpp => products/BellHybrid/apps/application-bell-background-sounds/windows/BGSoundsVolumeWindow.cpp +6 -6
@@ 36,19 36,19 @@ namespace gui
        topMessage->drawUnderline(false);

        auto data = presenter->getVolumeData();
        spinner   = new UIntegerSpinner({static_cast<UIntegerSpinner::Type>(data.min),
                                       static_cast<UIntegerSpinner::Type>(data.max),
                                       static_cast<UIntegerSpinner::Type>(data.step)},
        spinner   = new UIntegerSpinner({static_cast<UIntegerSpinner::value_type>(data.min),
                                       static_cast<UIntegerSpinner::value_type>(data.max),
                                       static_cast<UIntegerSpinner::value_type>(data.step)},
                                      Boundaries::Fixed);
        spinner->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::center_layout_h);
        spinner->setFont(bgSoundsStyle::valumeValueFont);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        spinner->setFocusEdges(RectangleEdge::None);
        spinner->setCurrentValue(static_cast<UIntegerSpinner::Type>(presenter->getVolume()));
        spinner->set_value(static_cast<UIntegerSpinner::value_type>(presenter->getVolume()));
        body->getCenterBox()->addWidget(spinner);

        spinner->onValueChanged = [this](const auto &value) { presenter->setVolume(value); };
        body->setMinMaxArrowsVisibility(spinner->getCurrentValue() == data.min, spinner->getCurrentValue() == data.max);
        body->setMinMaxArrowsVisibility(spinner->value() == data.min, spinner->value() == data.max);

        setFocusItem(body);
        body->resize();


@@ 63,7 63,7 @@ namespace gui
        resetTimer();
        auto data              = presenter->getVolumeData();
        const auto ret         = body->onInput(inputEvent);
        const auto selectedVal = spinner->getCurrentValue();
        const auto selectedVal = spinner->value();
        body->setMinMaxArrowsVisibility(selectedVal == data.min, selectedVal == data.max);
        return ret;
    }

M products/BellHybrid/apps/application-bell-main/windows/BellMainMenuWindow.cpp => products/BellHybrid/apps/application-bell-main/windows/BellMainMenuWindow.cpp +2 -2
@@ 7,7 7,7 @@
#include <application-bell-background-sounds/ApplicationBellBackgroundSounds.hpp>
#include <application-bell-bedtime/ApplicationBellBedtime.hpp>
#include <application-bell-main/ApplicationBellMain.hpp>
#include <application-bell-meditation-timer/ApplicationBellMeditationTimer.hpp>
#include <application-bell-meditation-timer/MeditationTimer.hpp>
#include <application-bell-settings/ApplicationBellSettings.hpp>
#include <application-bell-powernap/ApplicationBellPowerNap.hpp>



@@ 49,7 49,7 @@ namespace gui
        addAppMenu(utils::translate("app_bellmain_alarm"), app::applicationBellAlarmName);
        addAppMenu(utils::translate("app_bellmain_power_nap"), app::applicationBellPowerNapName);
        addAppMenu(utils::translate("app_bellmain_background_sounds"), app::applicationBellBackgroundSoundsName);
        addAppMenu(utils::translate("app_bellmain_meditation_timer"), app::applicationBellMeditationTimerName);
        addAppMenu(utils::translate("app_bellmain_meditation_timer"), app::MeditationTimer::defaultName);
        addAppMenu(utils::translate("app_bellmain_bedtime"), app::applicationBellBedtimeName);
        addAppMenu(utils::translate("app_bellmain_settings"), app::applicationBellSettingsName);


M products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt => products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt +11 -6
@@ 18,18 18,24 @@ target_include_directories(application-bell-meditation-timer

target_sources(application-bell-meditation-timer
    PRIVATE
        ApplicationBellMeditationTimer.cpp
        presenter/IntervalChimePresenter.cpp
        MeditationTimer.cpp
        models/ChimeInterval.cpp
        models/ChimeVolume.cpp
        models/StartDelay.cpp
        presenter/MeditationProgressPresenter.cpp
        presenter/MeditationTimerPresenter.cpp
        presenter/ReadyGoingPresenter.cpp
        presenter/SessionEndedPresenter.cpp 
        windows/IntervalChimeWindow.cpp
        presenter/SessionEndedPresenter.cpp
        presenter/SettingsPresenter.cpp
        presenter/StatisticsPresenter.cpp
        windows/MeditationMainWindow.cpp
        windows/MeditationRunningWindow.cpp
        windows/MeditationTimerWindow.cpp
        windows/ReadyGoingWindow.cpp
        windows/SettingsWindow.cpp
        windows/StatisticsWindow.cpp
    PUBLIC
        include/application-bell-meditation-timer/ApplicationBellMeditationTimer.hpp
        include/application-bell-meditation-timer/MeditationTimer.hpp
)

target_link_libraries(application-bell-meditation-timer


@@ 38,7 44,6 @@ target_link_libraries(application-bell-meditation-timer
        bell::audio
        bell::app-common
        bell::app-main
        bellgui
        service-appmgr
        service-time
    PUBLIC

R products/BellHybrid/apps/application-bell-meditation-timer/ApplicationBellMeditationTimer.cpp => products/BellHybrid/apps/application-bell-meditation-timer/MeditationTimer.cpp +50 -23
@@ 1,11 1,21 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ApplicationBellMeditationTimer.hpp"
#include "windows/IntervalChimeWindow.hpp"
#include "MeditationTimer.hpp"
#include "MeditationCommon.hpp"
#include "windows/MeditationMainWindow.hpp"
#include "windows/MeditationRunningWindow.hpp"
#include "windows/MeditationTimerWindow.hpp"
#include "windows/ReadyGoingWindow.hpp"
#include "windows/SettingsWindow.hpp"
#include "windows/StatisticsWindow.hpp"

#include "models/ChimeVolume.hpp"
#include "models/StartDelay.hpp"
#include "models/ChimeInterval.hpp"

#include "presenter/SettingsPresenter.hpp"
#include "presenter/StatisticsPresenter.hpp"

#include <common/models/TimeModel.hpp>
#include <common/windows/BellFinishedWindow.hpp>


@@ 13,48 23,65 @@

namespace app
{
    ApplicationBellMeditationTimer::ApplicationBellMeditationTimer(std::string name,
                                                                   std::string parent,
                                                                   StatusIndicators statusIndicators,
                                                                   StartInBackground startInBackground,
                                                                   uint32_t stackDepth)
    MeditationTimer::MeditationTimer(std::string name,
                                     std::string parent,
                                     StatusIndicators statusIndicators,
                                     StartInBackground startInBackground,
                                     uint32_t stackDepth)
        : Application(std::move(name), std::move(parent), statusIndicators, startInBackground, stackDepth)
    {}

    ApplicationBellMeditationTimer::~ApplicationBellMeditationTimer() = default;

    sys::ReturnCodes ApplicationBellMeditationTimer::InitHandler()
    sys::ReturnCodes MeditationTimer::InitHandler()
    {
        auto ret = Application::InitHandler();
        if (ret != sys::ReturnCodes::Success) {
            return ret;
        }

        chimeIntervalModel = std::make_unique<meditation::models::ChimeInterval>(this);
        chimeVolumeModel   = std::make_unique<meditation::models::ChimeVolume>(this);
        startDelayModel    = std::make_unique<meditation::models::StartDelay>(this);

        createUserInterface();

        return sys::ReturnCodes::Success;
    }

    void ApplicationBellMeditationTimer::createUserInterface()
    void MeditationTimer::createUserInterface()
    {
        windowsFactory.attach(gui::name::window::main_window, [this](ApplicationCommon *app, const std::string &name) {
            auto presenter = std::make_unique<app::meditation::MeditationTimerPresenter>(app, settings.get());
            return std::make_unique<gui::MeditationTimerWindow>(app, std::move(presenter));
        windowsFactory.attach(
            meditation::MeditationMainWindow::defaultName, [this](ApplicationCommon *app, const std::string &name) {
                auto presenter = std::make_unique<app::meditation::MeditationTimerPresenter>(app, settings.get());
                return std::make_unique<meditation::MeditationMainWindow>(app);
            });

        windowsFactory.attach(meditation::SettingsWindow::name,
                              [this](ApplicationCommon *app, const std::string &name) {
                                  auto presenter = std::make_unique<app::meditation::SettingsPresenter>(
                                      app, *chimeIntervalModel, *chimeVolumeModel, *startDelayModel);
                                  return std::make_unique<meditation::SettingsWindow>(app, std::move(presenter));
                              });

        windowsFactory.attach(meditation::StatisticsWindow::name, [](ApplicationCommon *app, const std::string &name) {
            auto presenter = std::make_unique<app::meditation::StatisticsPresenter>();
            return std::make_unique<meditation::StatisticsWindow>(app, std::move(presenter));
        });

        windowsFactory.attach(
            gui::name::window::intervalChime, [this](ApplicationCommon *app, const std::string &name) {
                auto presenter = std::make_unique<app::meditation::IntervalChimePresenter>(app, settings.get());
                return std::make_unique<gui::IntervalChimeWindow>(app, std::move(presenter));
            meditation::MeditationTimerWindow::name, [this](ApplicationCommon *app, const std::string &name) {
                auto presenter = std::make_unique<app::meditation::MeditationTimerPresenter>(app, settings.get());
                return std::make_unique<meditation::MeditationTimerWindow>(app, std::move(presenter));
            });
        windowsFactory.attach(gui::name::window::readyGoing, [](ApplicationCommon *app, const std::string &name) {
            auto presenter = std::make_unique<app::meditation::ReadyGoingPresenter>(app);

        windowsFactory.attach(meditation::windows::readyGoing, [this](ApplicationCommon *app, const std::string &name) {
            auto presenter = std::make_unique<app::meditation::ReadyGoingPresenter>(app, *startDelayModel);
            return std::make_unique<gui::ReadyGoingWindow>(app, std::move(presenter));
        });
        windowsFactory.attach(gui::name::window::meditationProgress,
        windowsFactory.attach(meditation::windows::meditationProgress,
                              [this](ApplicationCommon *app, const std::string &name) {
                                  auto timeModel = std::make_unique<app::TimeModel>();
                                  auto presenter = std::make_unique<app::meditation::MeditationProgressPresenter>(
                                      app, settings.get(), std::move(timeModel));
                                      app, settings.get(), std::move(timeModel), *chimeIntervalModel);
                                  return std::make_unique<gui::MeditationRunningWindow>(app, std::move(presenter));
                              });
        windowsFactory.attach(gui::window::session_paused::sessionPaused,


@@ 73,8 100,7 @@ namespace app
                      gui::popup::ID::BedtimeNotification});
    }

    sys::MessagePointer ApplicationBellMeditationTimer::DataReceivedHandler(sys::DataMessage *msgl,
                                                                            sys::ResponseMessage *resp)
    sys::MessagePointer MeditationTimer::DataReceivedHandler(sys::DataMessage *msgl, sys::ResponseMessage *resp)
    {
        auto retMsg = Application::DataReceivedHandler(msgl);
        if (auto response = dynamic_cast<sys::ResponseMessage *>(retMsg.get());


@@ 84,4 110,5 @@ namespace app

        return handleAsyncResponse(resp);
    }
    MeditationTimer::~MeditationTimer() = default;
} // namespace app

A products/BellHybrid/apps/application-bell-meditation-timer/data/Contract.hpp => products/BellHybrid/apps/application-bell-meditation-timer/data/Contract.hpp +27 -0
@@ 0,0 1,27 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <apps-common/BasePresenter.hpp>
#include <widgets/ListItemProvider.hpp>

namespace app::meditation::contract
{
    class View
    {
      public:
        virtual ~View() noexcept = default;
    };

    class Presenter : public BasePresenter<View>
    {
      public:
        virtual ~Presenter() noexcept                                                   = default;
        virtual auto getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider> = 0;
        virtual void loadData()                                                         = 0;
        virtual void saveData()                                                         = 0;
        virtual void eraseProviderData()                                                = 0;
        virtual void handleEnter()                                                      = 0;
    };
} // namespace app::meditation::contract

M products/BellHybrid/apps/application-bell-meditation-timer/data/MeditationCommon.hpp => products/BellHybrid/apps/application-bell-meditation-timer/data/MeditationCommon.hpp +7 -1
@@ 5,6 5,12 @@

namespace app::meditation
{
    namespace windows
    {
        static constexpr auto meditationProgress = "MeditationProgress";
        static constexpr auto readyGoing         = "MeditationReadyGoing";
        static constexpr auto sessionEnded       = "MeditationSessionEnded";
    }; // namespace windows

    constexpr auto meditationDBRecordName = "MeditationTimer";
    constexpr auto intervalDBRecordName   = "IntervalChime";
} // namespace app::meditation

R products/BellHybrid/apps/application-bell-meditation-timer/include/application-bell-meditation-timer/ApplicationBellMeditationTimer.hpp => products/BellHybrid/apps/application-bell-meditation-timer/include/application-bell-meditation-timer/MeditationTimer.hpp +20 -17
@@ 5,28 5,26 @@

#include <Application.hpp>

namespace gui::name::window
namespace app::meditation::models
{
    inline constexpr auto intervalChime      = "IntervalChimeWindow";
    inline constexpr auto meditationProgress = "MeditationProgressWindow";
    inline constexpr auto meditationTimer    = "MeditationTimerWindow";
    inline constexpr auto readyGoing         = "ReadyGoingWindow";
    inline constexpr auto sessionEnded       = "SessionEndedWindow";
} // namespace gui::name::window
    class ChimeInterval;
    class ChimeVolume;
    class StartDelay;
} // namespace app::meditation::models

namespace app
{
    inline constexpr auto applicationBellMeditationTimerName = "ApplicationBellMeditationTimer";

    class ApplicationBellMeditationTimer : public Application
    class MeditationTimer : public Application
    {
      public:
        ApplicationBellMeditationTimer(std::string name                    = applicationBellMeditationTimerName,
                                       std::string parent                  = "",
                                       StatusIndicators statusIndicators   = StatusIndicators{},
                                       StartInBackground startInBackground = {false},
                                       uint32_t stackDepth                 = 4096 * 2);
        ~ApplicationBellMeditationTimer();
        static constexpr auto defaultName = "ApplicationMeditationTimer";

        explicit MeditationTimer(std::string name                    = defaultName,
                                 std::string parent                  = "",
                                 StatusIndicators statusIndicators   = StatusIndicators{},
                                 StartInBackground startInBackground = {false},
                                 uint32_t stackDepth                 = 4096 * 2);
        ~MeditationTimer();

        sys::ReturnCodes InitHandler() override;



@@ 40,9 38,14 @@ namespace app
        {
            return sys::ReturnCodes::Success;
        }

      private:
        std::unique_ptr<app::meditation::models::ChimeInterval> chimeIntervalModel;
        std::unique_ptr<app::meditation::models::ChimeVolume> chimeVolumeModel;
        std::unique_ptr<app::meditation::models::StartDelay> startDelayModel;
    };

    template <> struct ManifestTraits<ApplicationBellMeditationTimer>
    template <> struct ManifestTraits<MeditationTimer>
    {
        static auto GetManifest() -> manager::ApplicationManifest
        {

A products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeInterval.cpp => products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeInterval.cpp +40 -0
@@ 0,0 1,40 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ChimeInterval.hpp"
#include <Utils.hpp>

namespace
{
    constexpr auto chime_interval_db = "chime_interval";

    /// Fraction numbers are stored using "n/d" format.
    app::list_items::FractionData to_fraction_data(const std::string &str)
    {
        const std::string delimiter = "/";
        std::string nominator       = str.substr(0, str.find(delimiter));
        std::string denominator     = str.substr(str.find(delimiter) + 1);
        return {utils::getNumericValue<int>(nominator), utils::getNumericValue<int>(denominator)};
    }
    std::string from_fraction_data(const app::list_items::FractionData &data)
    {
        return std::to_string(data.nominator) + "/" + std::to_string(data.denominator);
    }
} // namespace

namespace app::meditation::models
{
    void ChimeInterval::setValue(app::list_items::FractionData value)
    {
        settings.setValue(chime_interval_db, from_fraction_data(value));
    }
    app::list_items::FractionData ChimeInterval::getValue() const
    {
        const auto value = settings.getValue(chime_interval_db);
        if (value.empty()) {
            /// If not found, return 1/1 which corresponds to 'OFF'
            return {1, 1};
        }
        return to_fraction_data(value);
    }
} // namespace app::meditation::models

A products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeInterval.hpp => products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeInterval.hpp +19 -0
@@ 0,0 1,19 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <common/models/SettingsModel.hpp>
#include <common/widgets/list_items/Fraction.hpp>

namespace app::meditation::models
{
    class ChimeInterval : public gui::SettingsModel<list_items::FractionData>
    {
      public:
        using SettingsModel::SettingsModel;

        void setValue(list_items::FractionData value) override;
        list_items::FractionData getValue() const override;
    };
} // namespace app::meditation::models

A products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeVolume.cpp => products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeVolume.cpp +18 -0
@@ 0,0 1,18 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ChimeVolume.hpp"

namespace app::meditation::models
{

    void ChimeVolume::setValue(std::uint8_t value)
    {
        /// Dummy implementation.
    }
    std::uint8_t ChimeVolume::getValue() const
    {
        /// Dummy implementation
        return 5;
    }
} // namespace app::meditation::models

A products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeVolume.hpp => products/BellHybrid/apps/application-bell-meditation-timer/models/ChimeVolume.hpp +18 -0
@@ 0,0 1,18 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <common/models/SettingsModel.hpp>

namespace app::meditation::models
{
    class ChimeVolume : public gui::SettingsModel<std::uint8_t>
    {
      public:
        using SettingsModel::SettingsModel;

        void setValue(std::uint8_t value) override;
        std::uint8_t getValue() const override;
    };
} // namespace app::meditation::models

A products/BellHybrid/apps/application-bell-meditation-timer/models/StartDelay.cpp => products/BellHybrid/apps/application-bell-meditation-timer/models/StartDelay.cpp +27 -0
@@ 0,0 1,27 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "StartDelay.hpp"
#include <Utils.hpp>

namespace
{
    constexpr auto start_delay_db = "start_delay";
} // namespace

namespace app::meditation::models
{

    void StartDelay::setValue(std::uint8_t value)
    {
        settings.setValue(start_delay_db, std::to_string(value));
    }
    std::uint8_t StartDelay::getValue() const
    {
        const auto value = settings.getValue(start_delay_db);
        if (value.empty()) {
            return 0;
        }
        return utils::getNumericValue<std::uint32_t>(value);
    }
} // namespace app::meditation::models

A products/BellHybrid/apps/application-bell-meditation-timer/models/StartDelay.hpp => products/BellHybrid/apps/application-bell-meditation-timer/models/StartDelay.hpp +18 -0
@@ 0,0 1,18 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <common/models/SettingsModel.hpp>

namespace app::meditation::models
{
    class StartDelay : public gui::SettingsModel<std::uint8_t>
    {
      public:
        using SettingsModel::SettingsModel;

        void setValue(std::uint8_t value) override;
        std::uint8_t getValue() const override;
    };
} // namespace app::meditation::models

D products/BellHybrid/apps/application-bell-meditation-timer/presenter/IntervalChimePresenter.cpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/IntervalChimePresenter.cpp +0 -104
@@ 1,104 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ApplicationBellMeditationTimer.hpp"
#include "IntervalChimePresenter.hpp"
#include "MeditationCommon.hpp"
#include "MeditationStyle.hpp"

#include <service-db/Settings.hpp>

namespace app::meditation
{
    constexpr auto intervalsList = {1, 2, 5, 10, 15, 30};

    IntervalChimePresenter::IntervalChimePresenter(app::ApplicationCommon *app, settings::Settings *settings)
        : app{app}, settings{settings}
    {
        initIntervalOptions();
    }

    std::vector<std::string> IntervalChimePresenter::getIntervals() const
    {
        std::vector<std::string> intervalNames;
        for (auto const &option : intervalOptions) {
            intervalNames.push_back(option.second);
        }
        return intervalNames;
    }

    std::string IntervalChimePresenter::getCurrentInterval() const
    {
        const auto value = settings->getValue(intervalDBRecordName, settings::SettingsScope::AppLocal);
        for (auto const &option : intervalOptions) {
            if (utils::to_string(option.first.count()) == value) {
                return option.second;
            }
        }
        // If not found, return "None"
        return intervalOptions.at(0).second;
    }

    std::string IntervalChimePresenter::getTimeUnitName(std::string value)
    {
        using namespace app::meditationStyle::mtStyle::list;
        for (auto const &option : intervalOptions) {
            if (option.second == value) {
                if (option.first == std::chrono::minutes{0}) {
                    return "";
                }
                else if (option.first == std::chrono::minutes{1}) {
                    return utils::translate(timeUnitSingular);
                }
                else if (option.first == std::chrono::minutes{2}) {
                    return utils::translate(timeUnitPlural);
                }
                else {
                    return utils::translate(timeUnitGenitive);
                }
            }
        }
        return "";
    }

    void IntervalChimePresenter::activate(std::string value)
    {
        for (auto const &option : intervalOptions) {
            if (option.second == value) {
                settings->setValue(
                    intervalDBRecordName, utils::to_string(option.first.count()), settings::SettingsScope::AppLocal);
                reinterpret_cast<app::Application *>(app)->suspendIdleTimer();
                app->switchWindow(gui::name::window::readyGoing);
                break;
            }
        }
    }

    void IntervalChimePresenter::initIntervalOptions()
    {
        intervalOptions.push_back({std::chrono::minutes{0}, utils::translate("app_bell_meditation_interval_none")});

        const auto duration =
            utils::getNumericValue<int>(settings->getValue(meditationDBRecordName, settings::SettingsScope::AppLocal));
        if (duration == 1) {
            settings->setValue(intervalDBRecordName, utils::to_string(0), settings::SettingsScope::AppLocal);
            reinterpret_cast<app::Application *>(app)->suspendIdleTimer();
            app->switchWindow(gui::name::window::readyGoing);
            return;
        }

        for (const auto &interval : intervalsList) {
            if (interval < duration) {
                intervalOptions.push_back({std::chrono::minutes{interval}, getIntervalString(interval)});
            }
        }
    }

    std::string IntervalChimePresenter::getIntervalString(std::uint32_t value)
    {
        const std::string toReplace = "%0";
        std::string temp            = utils::translate("app_bell_meditation_interval_every_x_minutes");
        temp.replace(temp.find(toReplace), toReplace.size(), std::to_string(value));
        return temp;
    }
} // namespace app::meditation

D products/BellHybrid/apps/application-bell-meditation-timer/presenter/IntervalChimePresenter.hpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/IntervalChimePresenter.hpp +0 -54
@@ 1,54 0,0 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <apps-common/ApplicationCommon.hpp>
#include <apps-common/BasePresenter.hpp>

namespace app
{
    class ApplicationCommon;
}

namespace app::meditation
{
    class IntervalChimeContract
    {
      public:
        class View
        {
          public:
            virtual ~View() = default;
        };

        class Presenter : public BasePresenter<IntervalChimeContract::View>
        {
          public:
            virtual ~Presenter() noexcept                          = default;
            virtual std::vector<std::string> getIntervals() const  = 0;
            virtual std::string getCurrentInterval() const         = 0;
            virtual std::string getTimeUnitName(std::string value) = 0;
            virtual void activate(std::string value)               = 0;
        };
    };

    class IntervalChimePresenter : public IntervalChimeContract::Presenter
    {
      public:
        IntervalChimePresenter(app::ApplicationCommon *app, settings::Settings *settings);

        std::vector<std::string> getIntervals() const override;
        std::string getCurrentInterval() const override;
        std::string getTimeUnitName(std::string value) override;
        void activate(std::string value) override;

      private:
        app::ApplicationCommon *app  = nullptr;
        settings::Settings *settings = nullptr;
        std::vector<std::pair<std::chrono::minutes, const std::string>> intervalOptions;

        void initIntervalOptions();
        std::string getIntervalString(std::uint32_t value);
    };
} // namespace app::meditation

M products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationProgressPresenter.cpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationProgressPresenter.cpp +14 -7
@@ 1,32 1,39 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ApplicationBellMeditationTimer.hpp"
#include "MeditationTimer.hpp"
#include "MeditationCommon.hpp"
#include "MeditationProgressPresenter.hpp"
#include "models/ChimeInterval.hpp"

#include <common/models/TimeModel.hpp>
#include <common/windows/BellFinishedWindow.hpp>
#include <common/windows/SessionPausedWindow.hpp>
#include <service-appmgr/Controller.hpp>
#include <service-db/Settings.hpp>

namespace
{
    constexpr std::chrono::minutes emptyValue{0};

    std::chrono::seconds to_interval(const double ratio, std::chrono::minutes total_duration)
    {
        const long interval = std::chrono::seconds{total_duration}.count() * ratio;
        return std::chrono::seconds{interval};
    }
} // namespace

namespace app::meditation
{
    MeditationProgressPresenter::MeditationProgressPresenter(app::ApplicationCommon *app,
                                                             settings::Settings *settings,
                                                             std::unique_ptr<AbstractTimeModel> timeModel)
        : app{app}, settings{settings}, timeModel{std::move(timeModel)}
                                                             std::unique_ptr<AbstractTimeModel> timeModel,
                                                             models::ChimeInterval &chimeIntervalModel)
        : app{app}, settings{settings}, timeModel{std::move(timeModel)}, chimeIntervalModel{chimeIntervalModel}
    {
        duration = std::chrono::minutes{
            utils::getNumericValue<int>(settings->getValue(meditationDBRecordName, settings::SettingsScope::AppLocal))};
        interval = std::chrono::minutes{
            utils::getNumericValue<int>(settings->getValue(intervalDBRecordName, settings::SettingsScope::AppLocal))};

        interval = to_interval(chimeIntervalModel.getValue().to_double(), duration);
    }

    void MeditationProgressPresenter::setTimer(std::unique_ptr<app::TimerWithCallbacks> &&_timer)


@@ 46,7 53,7 @@ namespace app::meditation
    void MeditationProgressPresenter::start()
    {
        reinterpret_cast<app::Application *>(app)->suspendIdleTimer();
        timer->reset(std::chrono::seconds(duration), std::chrono::seconds(interval));
        timer->reset(std::chrono::seconds(duration), interval);
        timer->start();
    }


M products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationProgressPresenter.hpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationProgressPresenter.hpp +9 -2
@@ 28,6 28,11 @@ namespace settings

namespace app::meditation
{
    namespace models
    {
        class ChimeInterval;
    } // namespace models

    class MeditationProgressContract
    {
      public:


@@ 62,8 67,9 @@ namespace app::meditation
        settings::Settings *settings = nullptr;
        std::unique_ptr<app::TimerWithCallbacks> timer;
        std::unique_ptr<AbstractTimeModel> timeModel;
        models::ChimeInterval &chimeIntervalModel;
        std::chrono::minutes duration;
        std::chrono::minutes interval;
        std::chrono::seconds interval;

        static constexpr auto endWindowTimeout = std::chrono::seconds{5};



@@ 73,7 79,8 @@ namespace app::meditation
      public:
        MeditationProgressPresenter(app::ApplicationCommon *app,
                                    settings::Settings *settings,
                                    std::unique_ptr<AbstractTimeModel> timeModel);
                                    std::unique_ptr<AbstractTimeModel> timeModel,
                                    models::ChimeInterval &chimeIntervalModel);

        void setTimer(std::unique_ptr<app::TimerWithCallbacks> &&_timer) override;
        void handleUpdateTimeEvent() override;

M products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationTimerPresenter.cpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationTimerPresenter.cpp +7 -7
@@ 1,7 1,7 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ApplicationBellMeditationTimer.hpp"
#include "MeditationTimer.hpp"
#include "MeditationCommon.hpp"
#include "MeditationStyle.hpp"
#include "MeditationTimerPresenter.hpp"


@@ 23,25 23,25 @@ namespace app::meditation
        : app{app}, settings{settings}
    {}

    std::uint32_t MeditationTimerPresenter::getMinValue()
    std::uint8_t MeditationTimerPresenter::getMinValue()
    {
        return spinnerMin;
    }

    std::uint32_t MeditationTimerPresenter::getMaxValue()
    std::uint8_t MeditationTimerPresenter::getMaxValue()
    {
        return spinnerMax;
    }

    std::uint32_t MeditationTimerPresenter::getStepValue()
    std::uint8_t MeditationTimerPresenter::getStepValue()
    {
        return spinnerStep;
    }

    std::uint32_t MeditationTimerPresenter::getCurrentValue()
    std::uint8_t MeditationTimerPresenter::getCurrentValue()
    {
        const auto value = settings->getValue(meditationDBRecordName, settings::SettingsScope::AppLocal);
        auto defTimer    = utils::getNumericValue<std::uint32_t>(value);
        auto defTimer    = utils::getNumericValue<std::uint8_t>(value);
        if (defTimer == emptyValue) {
            defTimer = defaultValue;
        }


@@ 65,6 65,6 @@ namespace app::meditation
    void MeditationTimerPresenter::activate(std::uint32_t value)
    {
        settings->setValue(meditationDBRecordName, utils::to_string(value), settings::SettingsScope::AppLocal);
        app->switchWindow(gui::name::window::intervalChime);
        app->switchWindow(windows::readyGoing);
    }
} // namespace app::meditation

M products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationTimerPresenter.hpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/MeditationTimerPresenter.hpp +8 -8
@@ 31,10 31,10 @@ namespace app::meditation
        {
          public:
            virtual ~Presenter() noexcept                            = default;
            virtual std::uint32_t getMinValue()                      = 0;
            virtual std::uint32_t getMaxValue()                      = 0;
            virtual std::uint32_t getStepValue()                     = 0;
            virtual std::uint32_t getCurrentValue()                  = 0;
            virtual std::uint8_t getMinValue()                       = 0;
            virtual std::uint8_t getMaxValue()                       = 0;
            virtual std::uint8_t getStepValue()                      = 0;
            virtual std::uint8_t getCurrentValue()                   = 0;
            virtual std::string getTimeUnitName(std::uint32_t value) = 0;
            virtual void activate(std::uint32_t value)               = 0;
        };


@@ 45,10 45,10 @@ namespace app::meditation
      public:
        MeditationTimerPresenter(app::ApplicationCommon *app, settings::Settings *settings);

        std::uint32_t getMinValue() override;
        std::uint32_t getMaxValue() override;
        std::uint32_t getStepValue() override;
        std::uint32_t getCurrentValue() override;
        std::uint8_t getMinValue() override;
        std::uint8_t getMaxValue() override;
        std::uint8_t getStepValue() override;
        std::uint8_t getCurrentValue() override;
        std::string getTimeUnitName(std::uint32_t value) override;
        void activate(std::uint32_t value) override;


M products/BellHybrid/apps/application-bell-meditation-timer/presenter/ReadyGoingPresenter.cpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/ReadyGoingPresenter.cpp +10 -3
@@ 1,16 1,23 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ApplicationBellMeditationTimer.hpp"
#include "MeditationTimer.hpp"
#include "MeditationCommon.hpp"
#include "ReadyGoingPresenter.hpp"
#include "models/StartDelay.hpp"

namespace app::meditation
{
    ReadyGoingPresenter::ReadyGoingPresenter(app::ApplicationCommon *app) : app{app}
    ReadyGoingPresenter::ReadyGoingPresenter(app::ApplicationCommon *app, const models::StartDelay &startDelayModel)
        : app{app}, startDelayModel{startDelayModel}
    {}

    void ReadyGoingPresenter::activate()
    {
        app->switchWindow(gui::name::window::meditationProgress);
        app->switchWindow(windows::meditationProgress);
    }
    std::chrono::seconds ReadyGoingPresenter::getStartDelay()
    {
        return std::chrono::seconds{startDelayModel.getValue()};
    }
} // namespace app::meditation

M products/BellHybrid/apps/application-bell-meditation-timer/presenter/ReadyGoingPresenter.hpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/ReadyGoingPresenter.hpp +10 -2
@@ 13,6 13,11 @@ namespace app

namespace app::meditation
{
    namespace models
    {
        class StartDelay;
    } // namespace models

    class ReadyGoingPresenterContract
    {
      public:


@@ 24,16 29,19 @@ namespace app::meditation
        class Presenter : public BasePresenter<ReadyGoingPresenterContract::View>
        {
          public:
            virtual void activate() = 0;
            virtual void activate()                      = 0;
            virtual std::chrono::seconds getStartDelay() = 0;
        };
    };

    class ReadyGoingPresenter : public ReadyGoingPresenterContract::Presenter
    {
        app::ApplicationCommon *app{};
        const models::StartDelay &startDelayModel;
        void activate() override;
        std::chrono::seconds getStartDelay() override;

      public:
        explicit ReadyGoingPresenter(app::ApplicationCommon *app);
        ReadyGoingPresenter(app::ApplicationCommon *app, const models::StartDelay &startDelayModel);
    };
} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/presenter/SettingsPresenter.cpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/SettingsPresenter.cpp +117 -0
@@ 0,0 1,117 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SettingsPresenter.hpp"
#include "MeditationMainWindow.hpp"

#include "models/ChimeInterval.hpp"
#include "models/ChimeVolume.hpp"
#include "models/StartDelay.hpp"

#include <ApplicationCommon.hpp>
#include <common/widgets/list_items/Fraction.hpp>
#include <common/widgets/list_items/Numeric.hpp>
#include <common/windows/BellFinishedWindow.hpp>
#include <common/LanguageUtils.hpp>
#include <apps-common/InternalModel.hpp>
#include <apps-common/widgets/spinners/Spinners.hpp>

namespace app::list_items
{
    struct StartDelayFormatter
    {
      public:
        StartDelayFormatter() : none(utils::translate("app_bell_meditation_interval_none"))
        {}
        std::string operator()(std::uint8_t value) const
        {
            return value == 0 ? none : std::to_string(value);
        }

      private:
        std::string none;
    };

    using StartDelaySpinner = gui::UIntegerSpinnerWithFormatter<StartDelayFormatter>;
    class StartDelay : public app::list_items::details::ListItemBase<StartDelaySpinner>
    {
      public:
        StartDelay(spinner_type::range &&range,
                   gui::AbstractSettingsModel<spinner_type::value_type> &model,
                   const std::string &topDescription    = "",
                   const std::string &bottomDescription = "")
            : app::list_items::details::ListItemBase<spinner_type>(
                  std::move(range), model, topDescription, bottomDescription)
        {}

      private:
        void control_bottom_description(const spinner_type::value_type &value) final
        {
            bottomText->setVisible(value != 0);
            bottomText->setRichText(utils::language::getCorrectSecondsNumeralForm(value));
        }
    };

} // namespace app::list_items

namespace app::meditation
{
    using namespace gui;
    SettingsPresenter::SettingsPresenter(app::ApplicationCommon *app,
                                         models::ChimeInterval &chimeIntervalModel,
                                         models::ChimeVolume &chimeVolumeModel,
                                         models::StartDelay &startDelayModel)
        : application{app}, chimeIntervalModel{chimeIntervalModel}, chimeVolumeModel{chimeVolumeModel},
          startDelayModel{startDelayModel}
    {
        auto chimeInterval =
            new list_items::Fraction{list_items::Fraction::spinner_type::range{{1, 1}, {1, 2}, {1, 3}, {1, 4}},
                                     chimeIntervalModel,
                                     utils::translate("app_bell_meditation_chime_interval"),
                                     utils::translate("app_bell_meditation_chime_interval_bottom")};

        auto startDelay = new list_items::StartDelay{list_items::StartDelay::spinner_type::range{0, 60, 1},
                                                     startDelayModel,
                                                     utils::translate("app_bell_meditation_start_delay"),
                                                     utils::translate("common_second_lower")};

        auto chimeVolume = new list_items::Numeric{list_items::Numeric::spinner_type::range{1, 10, 1},
                                                   chimeVolumeModel,
                                                   utils::translate("app_bell_meditation_chime_volume")};

        listItemsProvider =
            std::make_shared<BellListItemProvider>(BellListItemProvider::Items{startDelay, chimeInterval, chimeVolume});

        for (auto &item : listItemsProvider->getListItems()) {
            item->setValue();
        }
    }
    void SettingsPresenter::loadData()
    {
        for (auto &item : listItemsProvider->getListItems()) {
            item->setValue();
        }
    }
    void SettingsPresenter::saveData()
    {
        for (auto &item : listItemsProvider->getListItems()) {
            item->getValue();
        }
    }
    auto SettingsPresenter::getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider>
    {
        return listItemsProvider;
    }
    void SettingsPresenter::eraseProviderData()
    {
        listItemsProvider->clearData();
    }
    void SettingsPresenter::handleEnter()
    {
        saveData();
        application->switchWindow(
            window::bell_finished::defaultName,
            BellFinishedWindowData::Factory::create("circle_success_big", MeditationMainWindow::defaultName));
    }

} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/presenter/SettingsPresenter.hpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/SettingsPresenter.hpp +44 -0
@@ 0,0 1,44 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "data/Contract.hpp"
#include <common/BellListItemProvider.hpp>
#include <memory>

namespace app
{
    class ApplicationCommon;
}

namespace app::meditation
{
    namespace models
    {
        class ChimeInterval;
        class ChimeVolume;
        class StartDelay;
    } // namespace models

    class SettingsPresenter : public contract::Presenter
    {
      public:
        SettingsPresenter(app::ApplicationCommon *app,
                          models::ChimeInterval &chimeIntervalModel,
                          models::ChimeVolume &chimeVolumeModel,
                          models::StartDelay &startDelayModel);
        void loadData() override;
        void saveData() override;
        auto getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider> override;
        void eraseProviderData() override;
        void handleEnter() override;

      private:
        ApplicationCommon *application{};
        models::ChimeInterval &chimeIntervalModel;
        models::ChimeVolume &chimeVolumeModel;
        models::StartDelay &startDelayModel;
        std::shared_ptr<BellListItemProvider> listItemsProvider;
    };
} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp +22 -0
@@ 0,0 1,22 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "StatisticsPresenter.hpp"

namespace app::meditation
{
    StatisticsPresenter::StatisticsPresenter()
    {}
    void StatisticsPresenter::eraseProviderData()
    {}
    void StatisticsPresenter::loadData()
    {}
    void StatisticsPresenter::saveData()
    {}
    auto StatisticsPresenter::getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider>
    {
        return std::shared_ptr<gui::ListItemProvider>();
    }
    void StatisticsPresenter::handleEnter()
    {}
} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp +23 -0
@@ 0,0 1,23 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "data/Contract.hpp"
#include <memory>

namespace app::meditation
{
    class StatisticsPresenter : public contract::Presenter
    {
      public:
        StatisticsPresenter();
        auto getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider> override;
        void loadData() override;
        void saveData() override;
        void eraseProviderData() override;
        void handleEnter() override;

      private:
    };
} // namespace app::meditation

D products/BellHybrid/apps/application-bell-meditation-timer/windows/IntervalChimeWindow.cpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/IntervalChimeWindow.cpp +0 -83
@@ 1,83 0,0 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ApplicationBellMeditationTimer.hpp"
#include "IntervalChimeWindow.hpp"
#include "MeditationStyle.hpp"

namespace gui
{
    IntervalChimeWindow::IntervalChimeWindow(
        app::ApplicationCommon *app,
        std::unique_ptr<app::meditation::IntervalChimeContract::Presenter> &&windowPresenter)
        : AppWindow(app, gui::name::window::intervalChime), presenter{std::move(windowPresenter)}
    {
        presenter->attach(this);
        buildInterface();
    }

    void IntervalChimeWindow::buildInterface()
    {
        AppWindow::buildInterface();

        statusBar->setVisible(false);
        header->setTitleVisibility(false);
        navBar->setVisible(false);

        body = new BellBaseLayout(this, 0, 0, style::window_width, style::window_height, true);

        auto topMessage = new TextFixedSize(body->firstBox);
        topMessage->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::outer_layouts_h);
        topMessage->setFont(style::window::font::largelight);
        topMessage->setEdges(gui::RectangleEdge::None);
        topMessage->activeItem = false;
        topMessage->setAlignment(Alignment(gui::Alignment::Horizontal::Center, gui::Alignment::Vertical::Center));
        topMessage->setText(utils::translate("app_bell_meditation_interval_chime"));
        topMessage->drawUnderline(false);

        auto titles             = presenter->getIntervals();
        spinner                 = new UTF8Spinner({titles.begin(), titles.end()}, Boundaries::Fixed);
        spinner->onValueChanged = [this](const auto val) { this->onValueChanged(val); };
        spinner->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::h);
        spinner->setFont(app::meditationStyle::icStyle::text::font);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        spinner->setFocusEdges(RectangleEdge::None);
        body->getCenterBox()->addWidget(spinner);

        auto currentValue = presenter->getCurrentInterval();
        spinner->setCurrentValue(currentValue);
        body->setMinMaxArrowsVisibility(currentValue == titles.front(), currentValue == titles.back());

        bottomDescription = new Label(body->lastBox);
        bottomDescription->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::outer_layouts_h);
        bottomDescription->setFont(app::meditationStyle::icStyle::minute::font);
        bottomDescription->setEdges(RectangleEdge::None);
        bottomDescription->activeItem = false;
        bottomDescription->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Top));
        bottomDescription->setText(presenter->getTimeUnitName(spinner->getCurrentValue()));

        setFocusItem(spinner);
        body->resize();
    }

    bool IntervalChimeWindow::onInput(const gui::InputEvent &inputEvent)
    {
        if (spinner->onInput(inputEvent)) {
            return true;
        }

        if (inputEvent.isShortRelease(gui::KeyCode::KEY_ENTER)) {
            presenter->activate(spinner->getCurrentValue());
            return true;
        }

        return AppWindow::onInput(inputEvent);
    }

    void IntervalChimeWindow::onValueChanged(const std::string currentValue)
    {
        auto titles = presenter->getIntervals();
        body->setMinMaxArrowsVisibility(currentValue == titles.front(), currentValue == titles.back());
        bottomDescription->setText(presenter->getTimeUnitName(spinner->getCurrentValue()));
    }
} // namespace gui

D products/BellHybrid/apps/application-bell-meditation-timer/windows/IntervalChimeWindow.hpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/IntervalChimeWindow.hpp +0 -36
@@ 1,36 0,0 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <apps-common/widgets/spinners/Spinners.hpp>
#include <widgets/BellBaseLayout.hpp>

#include <Application.hpp>
#include <AppWindow.hpp>
#include <InputEvent.hpp>

#include "IntervalChimePresenter.hpp"

namespace gui
{
    class IntervalChimeWindow : public AppWindow, public app::meditation::IntervalChimeContract::View
    {
      public:
        explicit IntervalChimeWindow(
            app::ApplicationCommon *app,
            std::unique_ptr<app::meditation::IntervalChimeContract::Presenter> &&windowPresenter);

        // virtual methods
        void buildInterface() override;
        bool onInput(const gui::InputEvent &inputEvent) override;

        void onValueChanged(const std::string currentValue);

      private:
        std::unique_ptr<app::meditation::IntervalChimeContract::Presenter> presenter;
        BellBaseLayout *body{};
        UTF8Spinner *spinner{};
        Label *bottomDescription{};
    };
} // namespace gui

A products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationMainWindow.cpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationMainWindow.cpp +58 -0
@@ 0,0 1,58 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "MeditationMainWindow.hpp"

#include "SettingsWindow.hpp"
#include "StatisticsWindow.hpp"
#include "MeditationTimerWindow.hpp"

#include <apps-common/messages/DialogMetadataMessage.hpp>
#include <common/options/OptionBellMenu.hpp>
#include <service-appmgr/messages/SwitchRequest.hpp>

namespace app::meditation
{
    using namespace gui;
    MeditationMainWindow::MeditationMainWindow(app::ApplicationCommon *app)
        : BellOptionWindow(app, gui::name::window::main_window)
    {
        addOptions(settingsOptionsList());
        setListTitle(utils::translate("app_bell_meditation_timer"));
    }

    std::list<Option> MeditationMainWindow::settingsOptionsList()
    {
        using ActivatedCallback = std::function<bool(gui::Item &)>;
        using Callback          = std::function<ActivatedCallback(const std::string &window)>;

        auto defaultCallback = [this](const std::string &window) {
            return [window, this](gui::Item &) {
                if (window.empty()) {
                    return false;
                }
                application->switchWindow(window);
                return true;
            };
        };

        std::list<gui::Option> settingsOptionList;
        auto addWinSettings = [&](const UTF8 &name, const std::string &window, Callback &&callback) {
            settingsOptionList.emplace_back(std::make_unique<gui::option::OptionBellMenu>(
                name,
                callback(window),
                [=](gui::Item &item) {
                    // put focus change callback here
                    return true;
                },
                this));
        };

        addWinSettings(utils::translate("app_bell_meditation_start"), MeditationTimerWindow::name, defaultCallback);
        addWinSettings(utils::translate("app_bell_meditation_settings"), SettingsWindow::name, defaultCallback);
        addWinSettings(utils::translate("app_bell_meditation_statistics"), StatisticsWindow::name, defaultCallback);

        return settingsOptionList;
    }

} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationMainWindow.hpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationMainWindow.hpp +20 -0
@@ 0,0 1,20 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <common/options/BellOptionWindow.hpp>

namespace app::meditation
{
    class MeditationMainWindow : public gui::BellOptionWindow
    {
      public:
        static constexpr auto defaultName = gui::name::window::main_window;
        explicit MeditationMainWindow(app::ApplicationCommon *app);

      private:
        std::list<gui::Option> settingsOptionsList();
    };

} // namespace app::meditation

M products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationRunningWindow.cpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationRunningWindow.cpp +3 -2
@@ 1,7 1,8 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ApplicationBellMeditationTimer.hpp"
#include "MeditationTimer.hpp"
#include "MeditationCommon.hpp"
#include "MeditationRunningWindow.hpp"
#include "MeditationStyle.hpp"



@@ 64,7 65,7 @@ namespace gui
    MeditationRunningWindow::MeditationRunningWindow(
        app::ApplicationCommon *app,
        std::unique_ptr<app::meditation::MeditationProgressContract::Presenter> &&windowPresenter)
        : AppWindow(app, gui::name::window::meditationProgress), presenter{std::move(windowPresenter)}
        : AppWindow(app, app::meditation::windows::meditationProgress), presenter{std::move(windowPresenter)}
    {
        presenter->attach(this);
        buildInterface();

M products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationTimerWindow.cpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationTimerWindow.cpp +12 -10
@@ 1,16 1,18 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ApplicationBellMeditationTimer.hpp"
#include "MeditationTimer.hpp"
#include "MeditationCommon.hpp"
#include "MeditationStyle.hpp"
#include "MeditationTimerWindow.hpp"

namespace gui
namespace app::meditation
{
    using namespace gui;
    MeditationTimerWindow::MeditationTimerWindow(
        app::ApplicationCommon *app,
        std::unique_ptr<app::meditation::MeditationTimerContract::Presenter> &&windowPresenter)
        : AppWindow(app, gui::name::window::meditationTimer), presenter{std::move(windowPresenter)}
        : AppWindow(app, name), presenter{std::move(windowPresenter)}
    {
        presenter->attach(this);
        buildInterface();


@@ 36,8 38,8 @@ namespace gui
        topMessage->drawUnderline(false);

        spinner = new UIntegerSpinner(
            UIntegerSpinner::Range{presenter->getMinValue(), presenter->getMaxValue(), presenter->getStepValue()},
            Boundaries::Fixed);
            UIntegerSpinner::range{presenter->getMinValue(), presenter->getMaxValue(), presenter->getStepValue()},
            gui::Boundaries::Fixed);
        spinner->onValueChanged = [this](const auto val) { this->onValueChanged(val); };
        spinner->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::h);
        spinner->setFont(app::meditationStyle::mtStyle::text::font);


@@ 47,7 49,7 @@ namespace gui
        body->getCenterBox()->addWidget(spinner);

        auto currentValue = presenter->getCurrentValue();
        spinner->setCurrentValue(currentValue);
        spinner->set_value(currentValue);
        body->setMinMaxArrowsVisibility(currentValue == presenter->getMinValue(),
                                        currentValue == presenter->getMaxValue());



@@ 57,7 59,7 @@ namespace gui
        bottomDescription->setEdges(RectangleEdge::None);
        bottomDescription->activeItem = false;
        bottomDescription->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Top));
        bottomDescription->setText(presenter->getTimeUnitName(spinner->getCurrentValue()));
        bottomDescription->setText(presenter->getTimeUnitName(spinner->value()));

        setFocusItem(spinner);
        body->resize();


@@ 70,7 72,7 @@ namespace gui
        }

        if (inputEvent.isShortRelease(gui::KeyCode::KEY_ENTER)) {
            presenter->activate(spinner->getCurrentValue());
            presenter->activate(spinner->value());
            return true;
        }



@@ 81,6 83,6 @@ namespace gui
    {
        body->setMinMaxArrowsVisibility(currentValue == presenter->getMinValue(),
                                        currentValue == presenter->getMaxValue());
        bottomDescription->setText(presenter->getTimeUnitName(spinner->getCurrentValue()));
        bottomDescription->setText(presenter->getTimeUnitName(spinner->value()));
    }
} // namespace gui
} // namespace app::meditation

M products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationTimerWindow.hpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/MeditationTimerWindow.hpp +7 -6
@@ 12,11 12,12 @@

#include "MeditationTimerPresenter.hpp"

namespace gui
namespace app::meditation
{
    class MeditationTimerWindow : public AppWindow, public app::meditation::MeditationTimerContract::View
    class MeditationTimerWindow : public gui::AppWindow, public app::meditation::MeditationTimerContract::View
    {
      public:
        static constexpr auto name = "MeditationTimerWindow";
        explicit MeditationTimerWindow(
            app::ApplicationCommon *app,
            std::unique_ptr<app::meditation::MeditationTimerContract::Presenter> &&windowPresenter);


@@ 29,8 30,8 @@ namespace gui

      private:
        std::unique_ptr<app::meditation::MeditationTimerContract::Presenter> presenter;
        BellBaseLayout *body{};
        UIntegerSpinner *spinner{};
        Label *bottomDescription{};
        gui::BellBaseLayout *body{};
        gui::UIntegerSpinner *spinner{};
        gui::Label *bottomDescription{};
    };
} // namespace gui
} // namespace app::meditation

M products/BellHybrid/apps/application-bell-meditation-timer/windows/ReadyGoingWindow.cpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/ReadyGoingWindow.cpp +8 -8
@@ 1,22 1,22 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ApplicationBellMeditationTimer.hpp"
#include "MeditationTimer.hpp"
#include "MeditationCommon.hpp"
#include "MeditationStyle.hpp"
#include "ReadyGoingWindow.hpp"

namespace
{
    constexpr inline auto defaultTimeout = std::chrono::seconds{10};
}

namespace gui
{
    ReadyGoingWindow::ReadyGoingWindow(
        app::ApplicationCommon *app,
        std::shared_ptr<app::meditation::ReadyGoingPresenterContract::Presenter> winPresenter)
        : WindowWithTimer(app, gui::name::window::readyGoing, defaultTimeout), presenter{std::move(winPresenter)}
        : WindowWithTimer(app, app::meditation::windows::readyGoing), presenter{std::move(winPresenter)}
    {
        const auto startDelay = presenter->getStartDelay();
        /// Even if the start delay is set to 0, give 'ReadyGoingWindow` 1s to display its contents
        resetTimer(startDelay == std::chrono::seconds{0} ? std::chrono::seconds{1} : startDelay);

        buildInterface();

        timerCallback = [this](Item &, sys::Timer &) {


@@ 44,7 44,7 @@ namespace gui
        }
    }

    bool ReadyGoingWindow::onInput(const InputEvent &inputEvent)
    bool ReadyGoingWindow::onInput(const InputEvent &)
    {
        return true;
    }

A products/BellHybrid/apps/application-bell-meditation-timer/windows/SettingsWindow.cpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/SettingsWindow.cpp +67 -0
@@ 0,0 1,67 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SettingsWindow.hpp"

#include <common/data/StyleCommon.hpp>
#include <apps-common/ApplicationCommon.hpp>
#include <module-gui/gui/input/InputEvent.hpp>
#include <module-gui/gui/widgets/SideListView.hpp>

namespace app::meditation
{
    using namespace gui;
    SettingsWindow::SettingsWindow(app::ApplicationCommon *app,
                                   std::unique_ptr<app::meditation::contract::Presenter> presenter)
        : AppWindow(app, name), presenter{std::move(presenter)}
    {
        this->presenter->attach(this);
        buildInterface();
    }

    void SettingsWindow::rebuild()
    {
        erase();
        buildInterface();
    }

    void SettingsWindow::buildInterface()
    {
        AppWindow::buildInterface();
        statusBar->setVisible(false);
        header->setTitleVisibility(false);
        navBar->setVisible(false);

        sideListView = new gui::SideListView(
            this, 0U, 0U, this->getWidth(), this->getHeight(), presenter->getPagesProvider(), PageBarType::None);
        sideListView->setEdges(RectangleEdge::None);

        sideListView->rebuildList(listview::RebuildType::Full);

        setFocusItem(sideListView);
    }

    void SettingsWindow::onBeforeShow(gui::ShowMode, gui::SwitchData *)
    {
        presenter->loadData();
        setFocusItem(sideListView);
    }

    bool SettingsWindow::onInput(const gui::InputEvent &inputEvent)
    {
        if (sideListView->onInput(inputEvent)) {
            return true;
        }
        if (inputEvent.isShortRelease(KeyCode::KEY_ENTER)) {
            presenter->handleEnter();
            return true;
        }

        return AppWindow::onInput(inputEvent);
    }

    void SettingsWindow::onClose(CloseReason)
    {
        presenter->eraseProviderData();
    }
} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/windows/SettingsWindow.hpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/SettingsWindow.hpp +33 -0
@@ 0,0 1,33 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "Contract.hpp"

#include <apps-common/windows/AppWindow.hpp>

namespace gui
{
    class SideListView;
}

namespace app::meditation
{
    class SettingsWindow : public gui::AppWindow, public app::meditation::contract::View
    {
      public:
        static constexpr auto name = "MeditationSettingsWindow";
        SettingsWindow(app::ApplicationCommon *app, std::unique_ptr<app::meditation::contract::Presenter> presenter);

        void buildInterface() override;
        void onBeforeShow(gui::ShowMode mode, gui::SwitchData *data) override;
        void onClose(CloseReason reason) override;
        bool onInput(const gui::InputEvent &inputEvent) override;
        void rebuild() override;

      private:
        gui::SideListView *sideListView{};
        std::unique_ptr<app::meditation::contract::Presenter> presenter;
    };
} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp +62 -0
@@ 0,0 1,62 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "StatisticsWindow.hpp"

#include "MeditationMainWindow.hpp"

#include <common/windows/BellFinishedWindow.hpp>
#include <common/data/StyleCommon.hpp>
#include <apps-common/ApplicationCommon.hpp>
#include <module-gui/gui/input/InputEvent.hpp>
#include <module-gui/gui/widgets/SideListView.hpp>
#include <apps-common/InternalModel.hpp>

namespace app::meditation
{
    using namespace gui;
    StatisticsWindow::StatisticsWindow(app::ApplicationCommon *app,
                                       std::unique_ptr<app::meditation::contract::Presenter> presenter)
        : AppWindow(app, name), presenter{std::move(presenter)}
    {
        this->presenter->attach(this);
        buildInterface();
    }

    void StatisticsWindow::rebuild()
    {
        erase();
        buildInterface();
    }

    void StatisticsWindow::buildInterface()
    {
        AppWindow::buildInterface();
        statusBar->setVisible(false);
        header->setTitleVisibility(false);
        navBar->setVisible(false);
    }

    void StatisticsWindow::onBeforeShow(gui::ShowMode mode, gui::SwitchData *data)
    {
        setFocusItem(sideListView);
    }

    bool StatisticsWindow::onInput(const gui::InputEvent &inputEvent)
    {
        if (sideListView->onInput(inputEvent)) {
            return true;
        }
        if (inputEvent.isShortRelease(KeyCode::KEY_ENTER)) {
            presenter->handleEnter();
            return true;
        }

        return AppWindow::onInput(inputEvent);
    }

    void StatisticsWindow::onClose(CloseReason reason)
    {
        presenter->eraseProviderData();
    }
} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp +33 -0
@@ 0,0 1,33 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "Contract.hpp"

#include <apps-common/windows/AppWindow.hpp>

namespace gui
{
    class SideListView;
}

namespace app::meditation
{
    class StatisticsWindow : public gui::AppWindow, public app::meditation::contract::View
    {
      public:
        static constexpr auto name = "MeditationStatisticsWindow";
        StatisticsWindow(app::ApplicationCommon *app, std::unique_ptr<app::meditation::contract::Presenter> presenter);

        void buildInterface() override;
        void onBeforeShow(gui::ShowMode mode, gui::SwitchData *data) override;
        void onClose(CloseReason reason) override;
        bool onInput(const gui::InputEvent &inputEvent) override;
        void rebuild() override;

      private:
        gui::SideListView *sideListView{};
        std::unique_ptr<app::meditation::contract::Presenter> presenter;
    };
} // namespace app::meditation

M products/BellHybrid/apps/application-bell-powernap/data/PowerNapListItem.cpp => products/BellHybrid/apps/application-bell-powernap/data/PowerNapListItem.cpp +5 -5
@@ 27,7 27,7 @@ namespace gui

    void PowerNapListItem::createSpinner()
    {
        spinner = new UIntegerSpinner(UIntegerSpinner::Range{spinnerMin, spinnerMax, spinnerStep}, Boundaries::Fixed);
        spinner = new UIntegerSpinner(UIntegerSpinner::range{spinnerMin, spinnerMax, spinnerStep}, Boundaries::Fixed);
        spinner->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::h);
        spinner->setFont(powerNapStyle::napPeriodFont);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));


@@ 65,7 65,7 @@ namespace gui

        inputCallback = [&](Item &, const InputEvent &inputEvent) -> bool {
            if (body->onInput(inputEvent)) {
                setBottomDescribtionText(utils::language::getCorrectMinutesNumeralForm(spinner->getCurrentValue()));
                setBottomDescribtionText(utils::language::getCorrectMinutesNumeralForm(spinner->value()));
                return true;
            }
            return false;


@@ 74,13 74,13 @@ namespace gui

    int PowerNapListItem::getSpinnerValue() const noexcept
    {
        return spinner->getCurrentValue();
        return spinner->value();
    }

    void PowerNapListItem::setSpinnerValue(int value)
    {
        spinner->setCurrentValue(value);
        setBottomDescribtionText(utils::language::getCorrectMinutesNumeralForm(spinner->getCurrentValue()));
        spinner->set_value(value);
        setBottomDescribtionText(utils::language::getCorrectMinutesNumeralForm(spinner->value()));
        onValueChanged(value);
    }


M products/BellHybrid/apps/application-bell-settings/ApplicationBellSettings.cpp => products/BellHybrid/apps/application-bell-settings/ApplicationBellSettings.cpp +1 -1
@@ 128,7 128,7 @@ namespace app
                    std::make_unique<SoundsRepository>(alarms::paths::getBedtimeReminderChimesDir());
                auto provider = std::make_shared<bell_settings::BedtimeSettingsListItemProvider>(
                    bedtimeModel, soundsRepository->getSongTitles());
                auto presenter = std::make_unique<bell_settings::BedtimeSettingsPresenter>(
                auto presenter = std::make_unique<bell_settings::SettingsPresenter>(
                    provider, bedtimeModel, *audioModel, std::move(soundsRepository));
                return std::make_unique<gui::BellSettingsBedtimeToneWindow>(app, std::move(presenter));
            });

M products/BellHybrid/apps/application-bell-settings/models/FrontlightListItemProvider.cpp => products/BellHybrid/apps/application-bell-settings/models/FrontlightListItemProvider.cpp +12 -11
@@ 4,7 4,8 @@
#include "FrontlightListItemProvider.hpp"
#include <common/models/FrontlightModel.hpp>
#include <gui/widgets/ListViewEngine.hpp>
#include <common/widgets/ListItems.hpp>
#include <common/widgets/list_items/Text.hpp>
#include <common/widgets/list_items/Numeric.hpp>

namespace app::bell_settings
{


@@ 16,11 17,11 @@ namespace app::bell_settings
        constexpr auto itemCount      = 2U;
        internalData.reserve(itemCount);

        auto brightness =
            new gui::NumListItem(model.getBrightnessModel(),
                                 gui::UIntegerSpinner::Range{brightnessMin, brightnessMax, brightnessStep},
                                 utils::translate("app_bell_settings_frontlight_top_message"));
        brightness->setOnValueChanged([this](const auto val) {
        auto brightness = new list_items::Numeric(
            list_items::Numeric::spinner_type::range{brightnessMin, brightnessMax, brightnessStep},
            model.getBrightnessModel(),
            utils::translate("app_bell_settings_frontlight_top_message"));
        brightness->set_on_value_change_cb([this](const auto val) {
            model.setStatus(true);
            model.setBrightness(val);
        });


@@ 29,12 30,12 @@ namespace app::bell_settings
        const auto modeAutoStr      = utils::translate("app_bell_settings_frontlight_mode_auto");
        const auto modeOnDemandsStr = utils::translate("app_bell_settings_frontlight_mode_on_demand");

        auto mode    = new gui::UTF8ListItem(model.getModeModel(),
                                          gui::UTF8Spinner::Range{modeOnDemandsStr, modeAutoStr},
                                          utils::translate("app_bell_settings_frontlight_mode_top_message"));
        auto mode    = new list_items::Text(list_items::Text::spinner_type ::range{modeOnDemandsStr, modeAutoStr},
                                         model.getModeModel(),
                                         utils::translate("app_bell_settings_frontlight_mode_top_message"));
        mode->onExit = [this, mode, modeAutoStr]() {
            model.setMode(mode->getCurrentValue() == modeAutoStr ? screen_light_control::ScreenLightMode::Automatic
                                                                 : screen_light_control::ScreenLightMode::Manual);
            model.setMode(mode->value() == modeAutoStr ? screen_light_control::ScreenLightMode::Automatic
                                                       : screen_light_control::ScreenLightMode::Manual);
        };
        internalData.emplace_back(mode);
    }

M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/AlarmSettingsListItemProvider.cpp => products/BellHybrid/apps/application-bell-settings/models/alarm_settings/AlarmSettingsListItemProvider.cpp +15 -12
@@ 4,6 4,8 @@
#include "BellSettingsStyle.hpp"
#include "AlarmSettingsListItemProvider.hpp"
#include <common/widgets/ListItems.hpp>
#include <common/widgets/list_items/Text.hpp>
#include <common/widgets/list_items/Numeric.hpp>
#include <apps-common/ApplicationCommon.hpp>

namespace app::bell_settings


@@ 22,22 24,22 @@ namespace app::bell_settings
        constexpr auto itemCount = 4U;
        internalData.reserve(itemCount);

        auto alarmTone = new UTF8ListItem(model.getAlarmTone(),
                                          std::move(alarmTonesRange),
                                          utils::translate("app_bell_settings_alarm_settings_tone"));
        alarmTone->setOnValueChanged([this](const UTF8 &val) {
        auto alarmTone = new list_items::Text(std::move(alarmTonesRange),
                                              model.getAlarmTone(),
                                              utils::translate("app_bell_settings_alarm_settings_tone"));
        alarmTone->set_on_value_change_cb([this](const auto &val) {
            if (onToneChange) {
                onToneChange(val);
            }
        });
        alarmTone->onEnter = [this, alarmTone]() {
            if (onToneEnter) {
                onToneEnter(alarmTone->getCurrentValue());
                onToneEnter(alarmTone->value());
            }
        };
        alarmTone->onExit = [this, alarmTone]() {
            if (onToneExit) {
                onToneExit(alarmTone->getCurrentValue());
                onToneExit(alarmTone->value());
            }
        };
        internalData.emplace_back(alarmTone);


@@ 45,22 47,23 @@ namespace app::bell_settings
        constexpr auto volumeStep = 1U;
        constexpr auto volumeMin  = 1U;
        constexpr auto volumeMax  = 10U;
        auto alarmVolume          = new NumListItem(model.getAlarmVolume(),
                                           UIntegerSpinner::Range{volumeMin, volumeMax, volumeStep},
                                           utils::translate("app_bell_settings_alarm_settings_volume"));
        alarmVolume->setOnValueChanged([this](const UIntegerSpinner::Type &val) {
        auto alarmVolume =
            new list_items::Numeric(list_items::Numeric::spinner_type::range{volumeMin, volumeMax, volumeStep},
                                    model.getAlarmVolume(),
                                    utils::translate("app_bell_settings_alarm_settings_volume"));
        alarmVolume->set_on_value_change_cb([this](const auto &val) {
            if (onVolumeChange) {
                onVolumeChange(val);
            }
        });
        alarmVolume->onEnter = [this, alarmTone]() {
            if (onVolumeEnter) {
                onVolumeEnter(alarmTone->getCurrentValue());
                onVolumeEnter(alarmTone->value());
            }
        };
        alarmVolume->onExit = [this, alarmVolume]() {
            if (onVolumeExit) {
                onVolumeExit(alarmVolume->getCurrentValue());
                onVolumeExit(alarmVolume->value());
            }
        };


M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/BedtimeSettingsListItemProvider.cpp => products/BellHybrid/apps/application-bell-settings/models/alarm_settings/BedtimeSettingsListItemProvider.cpp +15 -13
@@ 3,7 3,8 @@

#include "BedtimeSettingsListItemProvider.hpp"
#include "BellSettingsStyle.hpp"
#include <common/widgets/ListItems.hpp>
#include <common/widgets/list_items/Numeric.hpp>
#include <common/widgets/list_items/Text.hpp>
#include <apps-common/ApplicationCommon.hpp>
#include <gui/widgets/ListViewEngine.hpp>



@@ 23,32 24,33 @@ namespace app::bell_settings
        constexpr auto itemCount = 2U;
        internalData.reserve(itemCount);

        auto chimeTone = new UTF8ListItem(model.get()->getBedtimeTone(),
                                          std::move(chimeToneRange),
                                          utils::translate("app_bell_settings_bedtime_settings_tone"));
        chimeTone->setOnValueChanged([this](const UTF8 &val) {
        auto chimeTone = new list_items::Text(std::move(chimeToneRange),
                                              model->getBedtimeTone(),
                                              utils::translate("app_bell_settings_bedtime_settings_tone"));
        chimeTone->set_on_value_change_cb([this](const auto &val) {
            if (onToneChange) {
                onToneChange(val);
            }
        });
        chimeTone->onEnter = [this, chimeTone]() {
            if (onToneEnter) {
                onToneEnter(chimeTone->getCurrentValue());
                onToneEnter(chimeTone->value());
            }
        };
        chimeTone->onExit = [this, chimeTone]() {
            if (onToneExit) {
                onToneExit(chimeTone->getCurrentValue());
                onToneExit(chimeTone->value());
            }
        };
        internalData.emplace_back(chimeTone);
        constexpr auto volumeStep = 1U;
        constexpr auto volumeMin  = 1U;
        constexpr auto volumeMax  = 10U;
        auto volume               = new NumListItem(model.get()->getBedtimeVolume(),
                                      UIntegerSpinner::Range{volumeMin, volumeMax, volumeStep},
                                      utils::translate("app_bell_settings_bedtime_settings_volume"));
        volume->setOnValueChanged([this](const UIntegerSpinner::Type &val) {
        auto volume =
            new list_items::Numeric(list_items::Numeric::spinner_type::range{volumeMin, volumeMax, volumeStep},
                                    model->getBedtimeVolume(),
                                    utils::translate("app_bell_settings_bedtime_settings_volume"));
        volume->set_on_value_change_cb([this](const auto &val) {
            if (onVolumeChange) {
                onVolumeChange(val);
            }


@@ 56,13 58,13 @@ namespace app::bell_settings

        volume->onEnter = [this, chimeTone]() {
            if (onVolumeEnter) {
                onVolumeEnter(chimeTone->getCurrentValue());
                onVolumeEnter(chimeTone->value());
            }
        };

        volume->onExit = [this, volume]() {
            if (onVolumeExit) {
                onVolumeExit(volume->getCurrentValue());
                onVolumeExit(volume->value());
            }
        };
        internalData.emplace_back(volume);

M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/PrewakeUpListItemProvider.cpp => products/BellHybrid/apps/application-bell-settings/models/alarm_settings/PrewakeUpListItemProvider.cpp +24 -29
@@ 2,7 2,10 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "PrewakeUpListItemProvider.hpp"
#include <common/widgets/list_items/NumberWithSuffix.hpp>
#include <common/widgets/list_items/Numeric.hpp>
#include <common/widgets/ListItems.hpp>
#include <common/widgets/list_items/Text.hpp>
#include <apps-common/ApplicationCommon.hpp>
#include <gui/widgets/ListViewEngine.hpp>
#include <utility>


@@ 23,20 26,15 @@ namespace app::bell_settings
        constexpr auto itemCount = 4U;
        internalData.reserve(itemCount);

        const UTF8 minStr             = utils::translate("common_minute_short");
        const auto chimeDurationRange = NumWithStringListItem::NumWithStringSpinner::Range{
            NumWithStringListItem::Value{utils::translate("app_settings_toggle_off")},
            NumWithStringListItem::Value{5, minStr},
            NumWithStringListItem::Value{10, minStr},
            NumWithStringListItem::Value{15, minStr}};
        auto chimeDuration = new NumWithStringListItem(
        const std::string minStr = utils::translate("common_minute_short");
        auto chimeDuration       = new list_items::NumberWithSuffix(
            list_items::NumberWithSuffix::spinner_type::range{0, 5, 10, 15},
            model.getChimeDuration(),
            chimeDurationRange,
            utils::translate("app_bell_settings_alarm_settings_prewake_up_chime_top_description"),
            utils::translate("app_bell_settings_alarm_settings_prewake_up_chime_bottom_description"));

        chimeDuration->onProceed = [chimeDuration, this]() {
            if (chimeDuration->isOff()) {
            if (chimeDuration->value() == 0) {
                constexpr auto lightDurationListIndex = 3U;
                list->rebuildList(gui::listview::RebuildType::OnOffset, lightDurationListIndex);
                return true;


@@ 46,22 44,23 @@ namespace app::bell_settings

        internalData.emplace_back(chimeDuration);

        auto chimeTone = new UTF8ListItem(model.getChimeTone(),
                                          std::move(chimeToneRange),
                                          utils::translate("app_bell_settings_alarm_settings_prewake_up_chime_tone"));
        chimeTone->setOnValueChanged([this](const UTF8 &val) {
        auto chimeTone =
            new list_items::Text(std::move(chimeToneRange),
                                 model.getChimeTone(),
                                 utils::translate("app_bell_settings_alarm_settings_prewake_up_chime_tone"));
        chimeTone->set_on_value_change_cb([this](const auto &val) {
            if (onToneChange) {
                onToneChange(val);
            }
        });
        chimeTone->onEnter = [this, chimeTone]() {
            if (onToneEnter) {
                onToneEnter(chimeTone->getCurrentValue());
                onToneEnter(chimeTone->value());
            }
        };
        chimeTone->onExit = [this, chimeTone]() {
            if (onToneExit) {
                onToneExit(chimeTone->getCurrentValue());
                onToneExit(chimeTone->value());
            }
        };
        internalData.emplace_back(chimeTone);


@@ 69,10 68,11 @@ namespace app::bell_settings
        constexpr auto volumeStep = 1U;
        constexpr auto volumeMin  = 1U;
        constexpr auto volumeMax  = 10U;
        auto volume               = new NumListItem(model.getChimeVolume(),
                                      UIntegerSpinner::Range{volumeMin, volumeMax, volumeStep},
                                      utils::translate("app_bell_settings_alarm_settings_prewake_up_chime_volume"));
        volume->setOnValueChanged([this](const UIntegerSpinner::Type &val) {
        auto volume =
            new list_items::Numeric(list_items::Numeric::spinner_type::range{volumeMin, volumeMax, volumeStep},
                                    model.getChimeVolume(),
                                    utils::translate("app_bell_settings_alarm_settings_prewake_up_chime_volume"));
        volume->set_on_value_change_cb([this](const auto &val) {
            if (onVolumeChange) {
                onVolumeChange(val);
            }


@@ 80,30 80,25 @@ namespace app::bell_settings

        volume->onEnter = [this, chimeTone]() {
            if (onVolumeEnter) {
                onVolumeEnter(chimeTone->getCurrentValue());
                onVolumeEnter(chimeTone->value());
            }
        };

        volume->onExit = [this, volume]() {
            if (onVolumeExit) {
                onVolumeExit(volume->getCurrentValue());
                onVolumeExit(volume->value());
            }
        };
        internalData.emplace_back(volume);

        const auto lightDurationRange = NumWithStringListItem::NumWithStringSpinner::Range{
            NumWithStringListItem::Value{utils::translate("app_settings_toggle_off")},
            NumWithStringListItem::Value{5, minStr},
            NumWithStringListItem::Value{10, minStr},
            NumWithStringListItem::Value{15, minStr}};
        auto lightDuration = new NumWithStringListItem(
        auto lightDuration = new list_items::NumberWithSuffix(
            list_items::NumberWithSuffix::spinner_type::range{0, 5, 10, 15},
            model.getLightDuration(),
            lightDurationRange,
            utils::translate("app_bell_settings_alarm_settings_prewake_up_light_top_description"),
            utils::translate("app_bell_settings_alarm_settings_prewake_up_light_bottom_description"));

        lightDuration->onReturn = [chimeDuration, this]() {
            if (chimeDuration->isOff()) {
            if (chimeDuration->value() == 0) {
                list->rebuildList(gui::listview::RebuildType::OnOffset, 0);
                return true;
            }

M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/SnoozeListItemProvider.cpp => products/BellHybrid/apps/application-bell-settings/models/alarm_settings/SnoozeListItemProvider.cpp +41 -42
@@ 2,6 2,9 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SnoozeListItemProvider.hpp"
#include <common/widgets/list_items/NumberWithSuffix.hpp>
#include <common/widgets/list_items/Numeric.hpp>
#include <common/widgets/list_items/Text.hpp>
#include <common/widgets/ListItems.hpp>
#include <common/LanguageUtils.hpp>



@@ 12,41 15,37 @@ namespace app::bell_settings
{
    using namespace gui;

    NumWithStringListItem::NumWithStringSpinner::Range getDefaultChimeIntervalRange()
    list_items::NumberWithSuffix::spinner_type::range getDefaultChimeIntervalRange()
    {
        const UTF8 minStr = utils::translate("common_minute_short");
        return {
            NumWithStringListItem::Value{utils::translate("app_settings_toggle_off")},
            NumWithStringListItem::Value{1, minStr},
            NumWithStringListItem::Value{2, minStr},
            NumWithStringListItem::Value{3, minStr},
            NumWithStringListItem::Value{5, minStr},
        };
        return {0, 1, 2, 3, 5};
    }

    NumWithStringListItem::NumWithStringSpinner::Range transformChimeIntervalsRange(const std::uint32_t chimeLength)
    list_items::NumberWithSuffix::spinner_type::range transformChimeIntervalsRange(const std::uint32_t chimeLength)
    {
        auto chimeIntervals = getDefaultChimeIntervalRange();

        chimeIntervals.erase(std::remove_if(chimeIntervals.begin(),
                                            chimeIntervals.end(),
                                            [chimeLength](const auto &e) {
                                                return e.getValue() && (e.getValue().value() >= chimeLength);
                                                if (e == chimeLength) {
                                                    return true;
                                                }
                                                return e and ((chimeLength % e) != 0);
                                            }),
                             chimeIntervals.end());

        return chimeIntervals;
    }

    std::optional<NumWithStringListItem::Value> calculateCurrentChimeIntervalValue(
        const NumWithStringListItem::NumWithStringSpinner::Range &range,
        const NumWithStringListItem::Value &chimeInterval)
    std::optional<list_items::NumberWithSuffix::spinner_type::value_type> calculateCurrentChimeIntervalValue(
        const list_items::NumberWithSuffix::spinner_type::range &range,
        const list_items::NumberWithSuffix::spinner_type::value_type &chimeInterval)
    {
        if (range.size() == 1) {
            return {};
        }

        if (chimeInterval.getValue() && (chimeInterval.getValue().value() >= range.back().getValue().value())) {
        if (chimeInterval >= range.back()) {
            return range.back();
        }
        else {


@@ 58,7 57,7 @@ namespace app::bell_settings
                                                   std::vector<UTF8> chimeTonesRange)
        : model{model}
    {
        buildListItems(chimeTonesRange);
        buildListItems(std::move(chimeTonesRange));
    }

    void SnoozeListItemProvider::buildListItems(std::vector<UTF8> chimeTonesRange)


@@ 76,13 75,13 @@ namespace app::bell_settings

        auto chimeLengthBottomDescription =
            utils::language::getCorrectMinutesNumeralForm(model.getSnoozeLength().getValue());
        ;
        auto chimeLength = new NumListItem(model.getSnoozeLength(),
                                           UIntegerSpinner::Range{snoozeLengthMin, snoozeLengthMax, snoozeLengthStep},
                                           utils::translate("app_bell_settings_alarm_settings_snooze_length"),
                                           chimeLengthBottomDescription);
        auto chimeLength = new list_items::Numeric(
            list_items::Numeric::spinner_type::range{snoozeLengthMin, snoozeLengthMax, snoozeLengthStep},
            model.getSnoozeLength(),
            utils::translate("app_bell_settings_alarm_settings_snooze_length"),
            chimeLengthBottomDescription);

        chimeLength->setOnValueChanged([chimeLength](const std::uint32_t &val) {
        chimeLength->set_on_value_change_cb([chimeLength](const auto &val) {
            chimeLength->setBottomDescribtionText(utils::language::getCorrectMinutesNumeralForm(val));
        });



@@ 94,9 93,9 @@ namespace app::bell_settings

        internalData.emplace_back(chimeLength);

        auto chimeInterval = new NumWithStringListItem(
            model.getSnoozeChimeInterval(),
        auto chimeInterval = new list_items::NumberWithSuffix(
            getDefaultChimeIntervalRange(),
            model.getSnoozeChimeInterval(),
            utils::translate("app_bell_settings_alarm_settings_snooze_chime_interval"),
            utils::translate("app_bell_settings_alarm_settings_snooze_chime_interval_bot_desc"));
        chimeLength->setValue();


@@ 105,16 104,15 @@ namespace app::bell_settings

        chimeInterval->onEnter = [chimeInterval, chimeLength, this]() {
            if (chimeLength != nullptr) {
                const auto currentChimeLength   = chimeLength->getCurrentValue();
                const auto currentChimeInterval = chimeInterval->getCurrentValue();
                const auto currentChimeLength   = chimeLength->value();
                const auto currentChimeInterval = chimeInterval->value();
                const auto calculatedRange      = transformChimeIntervalsRange(currentChimeLength);
                const auto calculatedChimeInterval =
                    calculateCurrentChimeIntervalValue(calculatedRange, currentChimeInterval);

                if (calculatedChimeInterval) {
                    chimeInterval->getSpinner()->setRange(calculatedRange);
                    chimeInterval->getSpinner()->setCurrentValue(calculatedChimeInterval.value());
                    chimeInterval->setArrowsVisibility();
                    chimeInterval->set_range(calculatedRange);
                    chimeInterval->set_value(calculatedChimeInterval.value());
                }
                else {
                    this->onExit();


@@ 123,26 121,27 @@ namespace app::bell_settings
            return false;
        };

        auto snoozeChimeTone = new UTF8ListItem(model.getSnoozeChimeTone(),
                                                std::move(chimeTonesRange),
                                                utils::translate("app_bell_settings_alarm_settings_snooze_chime_tone"));
        snoozeChimeTone->setOnValueChanged([this](const UTF8 &val) {
        auto snoozeChimeTone =
            new list_items::Text(std::move(chimeTonesRange),
                                 model.getSnoozeChimeTone(),
                                 utils::translate("app_bell_settings_alarm_settings_snooze_chime_tone"));
        snoozeChimeTone->set_on_value_change_cb([this](const auto &val) {
            if (onToneChange) {
                onToneChange(val);
            }
        });
        snoozeChimeTone->onEnter = [this, snoozeChimeTone, chimeInterval]() {
            if (chimeInterval->isOff()) {
            if (chimeInterval->value() == 0) {
                this->onExit();
                return;
            }
            if (onToneEnter) {
                onToneEnter(snoozeChimeTone->getCurrentValue());
                onToneEnter(snoozeChimeTone->value());
            }
        };
        snoozeChimeTone->onExit = [this, snoozeChimeTone]() {
            if (onToneExit) {
                onToneExit(snoozeChimeTone->getCurrentValue());
                onToneExit(snoozeChimeTone->value());
            }
        };
        internalData.emplace_back(snoozeChimeTone);


@@ 151,10 150,10 @@ namespace app::bell_settings
        constexpr auto volumeMin  = 1U;
        constexpr auto volumeMax  = 10U;
        auto snoozeChimeVolume =
            new NumListItem(model.getSnoozeChimeVolume(),
                            UIntegerSpinner::Range{volumeMin, volumeMax, volumeStep},
                            utils::translate("app_bell_settings_alarm_settings_snooze_chime_volume"));
        snoozeChimeVolume->setOnValueChanged([this](const UIntegerSpinner::Type &val) {
            new list_items::Numeric(list_items::Numeric::spinner_type::range{volumeMin, volumeMax, volumeStep},
                                    model.getSnoozeChimeVolume(),
                                    utils::translate("app_bell_settings_alarm_settings_snooze_chime_volume"));
        snoozeChimeVolume->set_on_value_change_cb([this](const auto &val) {
            if (onVolumeChange) {
                onVolumeChange(val);
            }


@@ 162,13 161,13 @@ namespace app::bell_settings

        snoozeChimeVolume->onEnter = [this, snoozeChimeTone]() {
            if (onVolumeEnter) {
                onVolumeEnter(snoozeChimeTone->getCurrentValue());
                onVolumeEnter(snoozeChimeTone->value());
            }
        };

        snoozeChimeVolume->onExit = [this, snoozeChimeVolume]() {
            if (onVolumeExit) {
                onVolumeExit(snoozeChimeVolume->getCurrentValue());
                onVolumeExit(snoozeChimeVolume->value());
            }
        };


M products/BellHybrid/apps/application-bell-settings/models/alarm_settings/SnoozeSettingsModel.cpp => products/BellHybrid/apps/application-bell-settings/models/alarm_settings/SnoozeSettingsModel.cpp +2 -3
@@ 3,7 3,6 @@

#include <models/alarm_settings/SnoozeSettingsModel.hpp>

#include <apps-common/ApplicationCommon.hpp>
#include <db/SystemSettings.hpp>

namespace


@@ 47,7 46,7 @@ namespace app::bell_settings

    std::uint8_t SnoozeLengthModel::getValue() const
    {
        return get_helper<std::uint8_t>(settings, bell::settings::Snooze::length).value_or(0);
        return get_helper<std::uint32_t>(settings, bell::settings::Snooze::length).value_or(0);
    }

    void SnoozeChimeIntervalModel::setValue(std::uint8_t value)


@@ 58,7 57,7 @@ namespace app::bell_settings

    std::uint8_t SnoozeChimeIntervalModel::getValue() const
    {
        return get_helper<std::uint8_t>(settings, bell::settings::Snooze::interval).value_or(0);
        return get_helper<std::uint32_t>(settings, bell::settings::Snooze::interval).value_or(0);
    }

    void SnoozeChimeToneModel::setValue(UTF8 value)

M products/BellHybrid/apps/application-bell-settings/presenter/BedtimeSettingsPresenter.cpp => products/BellHybrid/apps/application-bell-settings/presenter/BedtimeSettingsPresenter.cpp +10 -10
@@ 5,10 5,10 @@

namespace app::bell_settings
{
    BedtimeSettingsPresenter::BedtimeSettingsPresenter(std::shared_ptr<BedtimeSettingsListItemProvider> provider,
                                                       std::shared_ptr<AbstractBedtimeModel> model,
                                                       AbstractAudioModel &audioModel,
                                                       std::unique_ptr<AbstractSoundsRepository> soundsRepository)
    SettingsPresenter::SettingsPresenter(std::shared_ptr<BedtimeSettingsListItemProvider> provider,
                                         std::shared_ptr<AbstractBedtimeModel> model,
                                         AbstractAudioModel &audioModel,
                                         std::unique_ptr<AbstractSoundsRepository> soundsRepository)
        : provider(std::move(provider)),
          model(std::move(model)), audioModel{audioModel}, soundsRepository{std::move(soundsRepository)}
    {


@@ 35,35 35,35 @@ namespace app::bell_settings
        };
    }

    auto BedtimeSettingsPresenter::saveData() -> void
    auto SettingsPresenter::saveData() -> void
    {
        for (const auto &item : provider->getListItems()) {
            item->getValue();
        }
    }

    auto BedtimeSettingsPresenter::loadData() -> void
    auto SettingsPresenter::loadData() -> void
    {
        for (const auto &item : provider->getListItems()) {
            item->setValue();
        }
    }

    auto BedtimeSettingsPresenter::getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider>
    auto SettingsPresenter::getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider>
    {
        return provider;
    }

    void BedtimeSettingsPresenter::eraseProviderData()
    void SettingsPresenter::eraseProviderData()
    {
        provider->clearData();
    }

    void BedtimeSettingsPresenter::stopSound()
    void SettingsPresenter::stopSound()
    {
        this->audioModel.stopPlayedByThis({});
    }
    void BedtimeSettingsPresenter::exitWithoutSave()
    void SettingsPresenter::exitWithoutSave()
    {
        model->getBedtimeVolume().restoreDefault();
    }

M products/BellHybrid/apps/application-bell-settings/presenter/BedtimeSettingsPresenter.hpp => products/BellHybrid/apps/application-bell-settings/presenter/BedtimeSettingsPresenter.hpp +5 -5
@@ 39,13 39,13 @@ namespace app::bell_settings
        };
    };

    class BedtimeSettingsPresenter : public BedtimeSettingsWindowContract::Presenter
    class SettingsPresenter : public BedtimeSettingsWindowContract::Presenter
    {
      public:
        BedtimeSettingsPresenter(std::shared_ptr<BedtimeSettingsListItemProvider> provider,
                                 std::shared_ptr<AbstractBedtimeModel> model,
                                 AbstractAudioModel &audioModel,
                                 std::unique_ptr<AbstractSoundsRepository> soundsRepository);
        SettingsPresenter(std::shared_ptr<BedtimeSettingsListItemProvider> provider,
                          std::shared_ptr<AbstractBedtimeModel> model,
                          AbstractAudioModel &audioModel,
                          std::unique_ptr<AbstractSoundsRepository> soundsRepository);

        auto getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider> override;
        auto saveData() -> void override;

M products/BellHybrid/apps/application-bell-settings/widgets/TemperatureUnitListItem.cpp => products/BellHybrid/apps/application-bell-settings/widgets/TemperatureUnitListItem.cpp +5 -6
@@ 15,14 15,14 @@ namespace gui
        setEdges(RectangleEdge::None);
        setFocusItem(body);

        temperatureUnit = new UTF8Spinner(
        temperatureUnit = new StringSpinner(
            {utils::temperature::celsiusDegreeSymbol, utils::temperature::fahrenheitDegreeSymbol}, Boundaries::Fixed);
        temperatureUnit->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::h);
        temperatureUnit->setFont(bell_settings_style::time_fmt_set_list_item::font);
        temperatureUnit->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        temperatureUnit->setFocusEdges(RectangleEdge::None);
        temperatureUnit->onValueChanged = [this](const auto &) {
            body->setMinMaxArrowsVisibility(temperatureUnit->isAtMin(), temperatureUnit->isAtMax());
            body->setMinMaxArrowsVisibility(temperatureUnit->is_min(), temperatureUnit->is_max());
        };

        body->getCenterBox()->addWidget(temperatureUnit);


@@ 44,13 44,12 @@ namespace gui

    auto TemperatureUnitListItem::getUnitAsStr() const noexcept -> UTF8
    {
        return temperatureUnit->getCurrentValue();
        return temperatureUnit->value();
    }
    auto TemperatureUnitListItem::setUnit(const utils::temperature::Temperature::Unit unit) -> void
    {
        using namespace utils::temperature;
        temperatureUnit->setCurrentValue(unit == Temperature::Unit::Celsius ? celsiusDegreeSymbol
                                                                            : fahrenheitDegreeSymbol);
        body->setMinMaxArrowsVisibility(temperatureUnit->isAtMin(), temperatureUnit->isAtMax());
        temperatureUnit->set_value(unit == Temperature::Unit::Celsius ? celsiusDegreeSymbol : fahrenheitDegreeSymbol);
        body->setMinMaxArrowsVisibility(temperatureUnit->is_min(), temperatureUnit->is_max());
    }
} // namespace gui

M products/BellHybrid/apps/application-bell-settings/widgets/TemperatureUnitListItem.hpp => products/BellHybrid/apps/application-bell-settings/widgets/TemperatureUnitListItem.hpp +1 -1
@@ 20,7 20,7 @@ namespace gui
        auto setUnit(utils::temperature::Temperature::Unit unit) -> void;

      private:
        UTF8Spinner *temperatureUnit{};
        StringSpinner *temperatureUnit{};
    };

} // namespace gui

M products/BellHybrid/apps/application-bell-settings/widgets/TimeFormatSetListItem.cpp => products/BellHybrid/apps/application-bell-settings/widgets/TimeFormatSetListItem.cpp +7 -7
@@ 26,13 26,13 @@ namespace gui
        setEdges(RectangleEdge::None);
        setFocusItem(body);

        timeFormat = new UTF8Spinner({fmtSpinner12H, fmtSpinner24H}, Boundaries::Fixed);
        timeFormat = new StringSpinner({fmtSpinner12H, fmtSpinner24H}, Boundaries::Fixed);
        timeFormat->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::center_layout_h);
        timeFormat->setFont(bell_settings_style::time_fmt_set_list_item::font);
        timeFormat->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        timeFormat->setFocusEdges(RectangleEdge::None);
        timeFormat->onValueChanged = [this](const auto &) {
            body->setMinMaxArrowsVisibility(timeFormat->isAtMin(), timeFormat->isAtMax());
            body->setMinMaxArrowsVisibility(timeFormat->is_min(), timeFormat->is_max());
        };

        body->getCenterBox()->addWidget(timeFormat);


@@ 64,19 64,19 @@ namespace gui

    auto TimeFormatSetListItem::getTimeFmt() const noexcept -> utils::time::Locale::TimeFormat
    {
        return timeFormat->getCurrentValue() == fmtSpinner12H ? utils::time::Locale::TimeFormat::FormatTime12H
                                                              : utils::time::Locale::TimeFormat::FormatTime24H;
        return timeFormat->value() == fmtSpinner12H ? utils::time::Locale::TimeFormat::FormatTime12H
                                                    : utils::time::Locale::TimeFormat::FormatTime24H;
    }

    auto TimeFormatSetListItem::setTimeFmt(utils::time::Locale::TimeFormat fmt) noexcept -> void
    {
        using namespace utils::time;
        if (fmt == Locale::TimeFormat::FormatTime12H) {
            timeFormat->setCurrentValue(fmtSpinner12H);
            timeFormat->set_value(fmtSpinner12H);
        }
        else if (fmt == Locale::TimeFormat::FormatTime24H) {
            timeFormat->setCurrentValue(fmtSpinner24H);
            timeFormat->set_value(fmtSpinner24H);
        }
        body->setMinMaxArrowsVisibility(timeFormat->isAtMin(), timeFormat->isAtMax());
        body->setMinMaxArrowsVisibility(timeFormat->is_min(), timeFormat->is_max());
    }
} // namespace gui

M products/BellHybrid/apps/application-bell-settings/widgets/TimeFormatSetListItem.hpp => products/BellHybrid/apps/application-bell-settings/widgets/TimeFormatSetListItem.hpp +1 -1
@@ 30,7 30,7 @@ namespace gui

      private:
        Label *bottomDescription{};
        UTF8Spinner *timeFormat;
        StringSpinner *timeFormat;
    };

} // namespace gui

M products/BellHybrid/apps/application-bell-settings/windows/BellSettingsBedtimeToneWindow.cpp => products/BellHybrid/apps/application-bell-settings/windows/BellSettingsBedtimeToneWindow.cpp +1 -1
@@ 11,7 11,7 @@
namespace gui
{
    BellSettingsBedtimeToneWindow::BellSettingsBedtimeToneWindow(
        app::ApplicationCommon *app, std::unique_ptr<app::bell_settings::BedtimeSettingsPresenter::Presenter> presenter)
        app::ApplicationCommon *app, std::unique_ptr<app::bell_settings::SettingsPresenter::Presenter> presenter)
        : AppWindow(app, name), presenter{std::move(presenter)}
    {
        this->presenter->attach(this);

M products/BellHybrid/apps/application-bell-settings/windows/BellSettingsBedtimeToneWindow.hpp => products/BellHybrid/apps/application-bell-settings/windows/BellSettingsBedtimeToneWindow.hpp +2 -3
@@ 17,8 17,7 @@ namespace gui
      public:
        static constexpr auto name = "BellSettingsBedtimeToneWindow";
        explicit BellSettingsBedtimeToneWindow(
            app::ApplicationCommon *app,
            std::unique_ptr<app::bell_settings::BedtimeSettingsPresenter::Presenter> presenter);
            app::ApplicationCommon *app, std::unique_ptr<app::bell_settings::SettingsPresenter::Presenter> presenter);

        void buildInterface() override;
        void onBeforeShow(gui::ShowMode mode, gui::SwitchData *data) override;


@@ 29,6 28,6 @@ namespace gui

      private:
        SideListView *sidelistview{};
        std::unique_ptr<app::bell_settings::BedtimeSettingsPresenter::Presenter> presenter;
        std::unique_ptr<app::bell_settings::SettingsPresenter::Presenter> presenter;
    };
} /* namespace gui */

M products/BellHybrid/apps/application-bell-settings/windows/BellSettingsLanguageWindow.cpp => products/BellHybrid/apps/application-bell-settings/windows/BellSettingsLanguageWindow.cpp +5 -5
@@ 36,17 36,17 @@ namespace gui
        topMessage->drawUnderline(false);

        auto data = presenter->getLanguages();
        spinner   = new UTF8Spinner({data.begin(), data.end()}, Boundaries::Fixed);
        spinner   = new StringSpinner({data.begin(), data.end()}, Boundaries::Fixed);
        spinner->setMaximumSize(style::bell_base_layout::w, style::bell_base_layout::center_layout_h);
        spinner->setFont(style::window::font::large);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        spinner->setFocusEdges(RectangleEdge::None);
        spinner->setCurrentValue(presenter->getSelectedLanguage());
        spinner->set_value(presenter->getSelectedLanguage());
        spinner->onValueChanged = [this](const auto &) {
            body->setMinMaxArrowsVisibility(spinner->isAtMin(), spinner->isAtMax());
            body->setMinMaxArrowsVisibility(spinner->is_min(), spinner->is_max());
        };
        body->getCenterBox()->addWidget(spinner);
        body->setMinMaxArrowsVisibility(spinner->isAtMin(), spinner->isAtMax());
        body->setMinMaxArrowsVisibility(spinner->is_min(), spinner->is_max());

        body->resize();
    }


@@ 62,7 62,7 @@ namespace gui
            return true;
        }
        else if (inputEvent.isShortRelease(KeyCode::KEY_ENTER)) {
            presenter->setLanguage(spinner->getCurrentValue());
            presenter->setLanguage(spinner->value());
            return true;
        }
        return AppWindow::onInput(inputEvent);

M products/BellHybrid/apps/application-bell-settings/windows/BellSettingsLanguageWindow.hpp => products/BellHybrid/apps/application-bell-settings/windows/BellSettingsLanguageWindow.hpp +1 -1
@@ 28,6 28,6 @@ namespace gui
        std::unique_ptr<app::bell_settings::LanguageWindowPresenter::Presenter> presenter;

        BellBaseLayout *body{};
        UTF8Spinner *spinner = nullptr;
        StringSpinner *spinner = nullptr;
    };
} // namespace gui

M products/BellHybrid/apps/common/include/common/BellListItemProvider.hpp => products/BellHybrid/apps/common/include/common/BellListItemProvider.hpp +11 -0
@@ 12,6 12,17 @@ namespace app
                                 public gui::ListItemProvider
    {
      public:
        using Items = std::vector<gui::BellSideListItemWithCallbacks *>;
        explicit BellListItemProvider(Items &&items)
        {
            for (const auto &item : items) {
                internalData.emplace_back(item);
                item->deleteByList = false;
            }
        }

        BellListItemProvider() = default;

        std::vector<gui::BellSideListItemWithCallbacks *> getListItems();

        auto requestRecords(uint32_t offset, uint32_t limit) -> void override;

M products/BellHybrid/apps/common/include/common/LanguageUtils.hpp => products/BellHybrid/apps/common/include/common/LanguageUtils.hpp +2 -1
@@ 7,5 7,6 @@

namespace utils::language
{
    auto getCorrectMinutesNumeralForm(int val) -> std::string;
    auto getCorrectMinutesNumeralForm(std::uint32_t val) -> std::string;
    auto getCorrectSecondsNumeralForm(std::uint32_t val) -> std::string;
} // namespace utils::language

M products/BellHybrid/apps/common/include/common/widgets/ListItems.hpp => products/BellHybrid/apps/common/include/common/widgets/ListItems.hpp +4 -62
@@ 6,7 6,6 @@
#include <common/models/AbstractSettingsModel.hpp>
#include <common/widgets/BellSideListItemWithCallbacks.hpp>

#include <apps-common/widgets/spinners/SpinnerContents.hpp>
#include <apps-common/widgets/spinners/Spinners.hpp>
#include <apps-common/widgets/TimeSetFmtSpinner.hpp>



@@ 15,6 14,7 @@

namespace gui
{

    class OnOffListItem : public BellSideListItemWithCallbacks
    {
      public:


@@ 25,67 25,9 @@ namespace gui
      private:
        void setArrowsVisibility();

        const UTF8 onStr;
        const UTF8 offStr;
        UTF8Spinner *spinner{};
    };

    class NumListItem : public BellSideListItemWithCallbacks
    {
      public:
        explicit NumListItem(AbstractSettingsModel<std::uint8_t> &model,
                             UIntegerSpinner::Range range,
                             const std::string &topDescription    = "",
                             const std::string &bottomDescription = "");

        void setOnValueChanged(std::function<void(const UIntegerSpinner::Type &)> &&cb);
        UIntegerSpinner::Type getCurrentValue();

      private:
        UIntegerSpinner *spinner{};

        void setArrowsVisibility(UIntegerSpinner::Range range);
    };

    class NumWithStringListItem : public BellSideListItemWithCallbacks
    {
      public:
        using Value                = NumWithString<std::uint32_t, UTF8>;
        using NumWithStringSpinner = GenericSpinner<StringPolicy<Value>>;

        explicit NumWithStringListItem(AbstractSettingsModel<std::uint8_t> &model,
                                       NumWithStringSpinner::Range range,
                                       const std::string &topDescription    = "",
                                       const std::string &bottomDescription = "");

        bool isOff() const;
        NumWithStringSpinner *getSpinner()
        {
            return spinner;
        }
        NumWithStringSpinner::Type getCurrentValue();
        void setArrowsVisibility();

      private:
        NumWithStringSpinner *spinner{};
        const UTF8 offStr;
        const UTF8 minStr;
    };

    class UTF8ListItem : public BellSideListItemWithCallbacks
    {
      public:
        explicit UTF8ListItem(AbstractSettingsModel<UTF8> &model,
                              UTF8Spinner::Range range,
                              const std::string &topDescription = "");

        void setOnValueChanged(std::function<void(const UTF8 &)> &&cb);
        UTF8Spinner::Type getCurrentValue();

      private:
        void setArrowsVisibility(const UTF8Spinner::Range &range);

        UTF8Spinner *spinner{};
        const std::string onStr;
        const std::string offStr;
        StringSpinner *spinner;
    };

    class TimeListItem : public BellSideListItemWithCallbacks

A products/BellHybrid/apps/common/include/common/widgets/list_items/Fraction.hpp => products/BellHybrid/apps/common/include/common/widgets/list_items/Fraction.hpp +60 -0
@@ 0,0 1,60 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "details.hpp"
#include <i18n/i18n.hpp>
#include <utility>

namespace app::list_items
{
    /// Representation of fraction number tailored for GUI/app use
    struct FractionData
    {
        bool operator==(const FractionData &oth) const
        {
            return denominator == oth.denominator and nominator == oth.nominator;
        }
        double to_double() const
        {
            return nominator == denominator ? 0 : static_cast<double>(nominator) / denominator;
        }
        int nominator;
        int denominator;
    };

    struct FractionFormatter
    {
      public:
        FractionFormatter() : off(utils::translate("app_settings_toggle_off"))
        {}
        std::string operator()(FractionData value) const
        {
            return value.nominator == value.denominator
                       ? off
                       : (std::to_string(value.nominator) + "/" + std::to_string(value.denominator));
        }

      private:
        std::string off;
    };

    using FractionSpinner = gui::StringOutputSpinner<Model<FractionData>, FractionFormatter>;
    class Fraction : public details::ListItemBase<FractionSpinner>
    {
      public:
        Fraction(spinner_type::range &&range,
                 gui::AbstractSettingsModel<FractionData> &model,
                 const std::string &topDescription    = "",
                 const std::string &bottomDescription = "")
            : details::ListItemBase<spinner_type>(std::move(range), model, topDescription, bottomDescription)
        {}

      private:
        void control_bottom_description(const FractionData &value) final
        {
            bottomText->setVisible(value.nominator != value.denominator);
        }
    };
} // namespace app::list_items

A products/BellHybrid/apps/common/include/common/widgets/list_items/NumberWithSuffix.hpp => products/BellHybrid/apps/common/include/common/widgets/list_items/NumberWithSuffix.hpp +45 -0
@@ 0,0 1,45 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "details.hpp"
#include <i18n/i18n.hpp>
#include <utility>

namespace app::list_items
{
    struct NumberWithSuffixFormatter
    {
      public:
        NumberWithSuffixFormatter()
            : suffix(utils::translate("common_minute_short")), off(utils::translate("app_settings_toggle_off"))
        {}
        std::string operator()(const std::uint8_t &value) const
        {
            return value == 0 ? off : (std::to_string(value) + " " + suffix);
        }

      private:
        std::string suffix;
        std::string off;
    };

    using NumberWithSuffixSpinner = gui::StringOutputSpinner<Model<std::uint8_t, true>, NumberWithSuffixFormatter>;
    class NumberWithSuffix : public details::ListItemBase<NumberWithSuffixSpinner>
    {
      public:
        NumberWithSuffix(spinner_type::range &&range,
                         gui::AbstractSettingsModel<spinner_type::value_type> &model,
                         const std::string &topDescription    = "",
                         const std::string &bottomDescription = "")
            : details::ListItemBase<spinner_type>(std::move(range), model, topDescription, bottomDescription)
        {}

      private:
        void control_bottom_description(const spinner_type::value_type &value) final
        {
            bottomText->setVisible(value != 0);
        }
    };
} // namespace app::list_items

A products/BellHybrid/apps/common/include/common/widgets/list_items/Numeric.hpp => products/BellHybrid/apps/common/include/common/widgets/list_items/Numeric.hpp +22 -0
@@ 0,0 1,22 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "details.hpp"
#include <i18n/i18n.hpp>
#include <utility>

namespace app::list_items
{
    class Numeric : public details::ListItemBase<gui::UIntegerSpinner>
    {
      public:
        Numeric(gui::UIntegerSpinner::range &&range,
                gui::AbstractSettingsModel<gui::UIntegerSpinner::value_type> &model,
                const std::string &topDescription    = "",
                const std::string &bottomDescription = "")
            : details::ListItemBase<gui::UIntegerSpinner>(std::move(range), model, topDescription, bottomDescription)
        {}
    };
} // namespace app::list_items

A products/BellHybrid/apps/common/include/common/widgets/list_items/OnOff.hpp => products/BellHybrid/apps/common/include/common/widgets/list_items/OnOff.hpp +30 -0
@@ 0,0 1,30 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "details.hpp"
#include <i18n/i18n.hpp>
#include <utility>

namespace app::list_items
{
    class OnOff : public details::ListItemBase<gui::StringSpinner>
    {
      public:
        explicit OnOff(gui::AbstractSettingsModel<gui::StringSpinner::value_type> &model,
                       const std::string &topDescription    = "",
                       const std::string &bottomDescription = "")
            : details::ListItemBase<gui::StringSpinner>(
                  {utils::translate("app_settings_toggle_off"), utils::translate("app_settings_toggle_on")},
                  model,
                  topDescription,
                  bottomDescription)
        {}

        [[nodiscard]] bool is_active() const
        {
            return value() == utils::translate("app_settings_toggle_on");
        }
    };
} // namespace app::list_items

A products/BellHybrid/apps/common/include/common/widgets/list_items/Text.hpp => products/BellHybrid/apps/common/include/common/widgets/list_items/Text.hpp +24 -0
@@ 0,0 1,24 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "details.hpp"
#include <i18n/i18n.hpp>
#include <utility>

namespace app::list_items
{
    class Text : public details::ListItemBase<gui::StringSpinner>
    {
      public:
        Text(gui::StringSpinner::range &&range,
             gui::AbstractSettingsModel<gui::StringSpinner::value_type> &model,
             const std::string &topDescription    = "",
             const std::string &bottomDescription = "")
            : details::ListItemBase<gui::StringSpinner>(std::move(range), model, topDescription, bottomDescription)
        {
            raw_spinner()->setFont(gui::bell_style::font_center);
        }
    };
} // namespace app::list_items

A products/BellHybrid/apps/common/include/common/widgets/list_items/details.hpp => products/BellHybrid/apps/common/include/common/widgets/list_items/details.hpp +106 -0
@@ 0,0 1,106 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <common/models/AbstractSettingsModel.hpp>
#include <common/widgets/BellSideListItemWithCallbacks.hpp>
#include <common/data/StyleCommon.hpp>

#include <apps-common/widgets/spinners/Spinners.hpp>
#include <apps-common/widgets/TimeSetFmtSpinner.hpp>

namespace app::list_items
{
    namespace details
    {
        template <typename SpinnerType> class ListItemBase : public gui::BellSideListItemWithCallbacks
        {
          public:
            using spinner_type = SpinnerType;
            using value_type   = typename SpinnerType::value_type;

            value_type value() const
            {
                return spinner->value();
            }

            void set_value(const value_type &value)
            {
                spinner->set_value(value);
            }

            void set_range(const typename spinner_type::range &range)
            {
                spinner->set_range(range);
            }

            void set_on_value_change_cb(std::function<void(const value_type &)> &&cb)
            {
                onValueChangeCb = std::move(cb);
            }

          protected:
            SpinnerType *raw_spinner()
            {
                return spinner;
            }

            /// Might be overridden if the specific handling of the bottom text is needed
            virtual void control_bottom_description(const value_type &value){};

            explicit ListItemBase(typename SpinnerType::range &&range,
                                  gui::AbstractSettingsModel<value_type> &model,
                                  const std::string &topDescription    = "",
                                  const std::string &bottomDescription = "")
                : BellSideListItemWithCallbacks(topDescription), model{model}, bottomDescription{bottomDescription}
            {
                spinner = new SpinnerType(std::move(range), gui::Boundaries::Fixed);
                spinner->setMaximumSize(::style::bell_base_layout::w, ::style::bell_base_layout::h);
                spinner->setFont(gui::bell_style::font);
                spinner->setAlignment(
                    gui::Alignment(gui::Alignment::Horizontal::Center, gui::Alignment::Vertical::Center));
                spinner->setFocusEdges(gui::RectangleEdge::None);
                body->getCenterBox()->addWidget(spinner);

                if (not this->bottomDescription.empty()) {
                    setupBottomDescription(this->bottomDescription);
                }

                spinner->onValueChanged = [this](const auto &val) {
                    control_visibility();
                    if (onValueChangeCb) {
                        onValueChangeCb(val);
                    }
                };

                getValue = [this]() { this->model.setValue(this->spinner->value()); };
                setValue = [this]() { this->spinner->set_value(this->model.getValue()); };

                inputCallback = [this, &bottomDescription](Item &, const gui::InputEvent &event) {
                    return OnInputCallback(event);
                };

                focusChangedCallback = [this, &bottomDescription](Item &) {
                    OnFocusChangedCallback();
                    control_visibility();
                    return true;
                };
            }

          private:
            void control_visibility()
            {
                body->setMinMaxArrowsVisibility(spinner->is_min(), spinner->is_max());
                if (not this->bottomDescription.empty()) {
                    control_bottom_description(spinner->value());
                }
            }
            SpinnerType *spinner{};
            gui::AbstractSettingsModel<value_type> &model;
            std::string bottomDescription;
            std::function<void(const value_type &)> onValueChangeCb;
        };
    } // namespace details

} // namespace app::list_items

M products/BellHybrid/apps/common/src/LanguageUtils.cpp => products/BellHybrid/apps/common/src/LanguageUtils.cpp +28 -6
@@ 3,21 3,43 @@

#include <i18n/i18n.hpp>

namespace utils::language
namespace
{
    auto getCorrectMinutesNumeralForm(int val) -> std::string
    std::string transform(const std::uint32_t val,
                          const std::string &minuteLower,
                          const std::string &minutesLower,
                          const std::string &minutesLowerGenitive)
    {
        if (val == 1) {
            return utils::translate("common_minute_lower");
            return minuteLower;
        }
        if (utils::getDisplayLanguage() == "Polski") {
            if (val < 10 || val > 20) {
                if ((val % 10) == 2 || (val % 10) == 3 || (val % 10) == 4) {
                    return utils::translate("common_minutes_lower");
                    return minutesLower;
                }
            }
            return utils::translate("common_minutes_lower_genitive");
            return minutesLowerGenitive;
        }
        return utils::translate("common_minutes_lower");
        return minutesLower;
    }
} // namespace

namespace utils::language
{
    auto getCorrectMinutesNumeralForm(const std::uint32_t val) -> std::string
    {
        return transform(val,
                         utils::translate("common_minute_lower"),
                         utils::translate("common_minutes_lower"),
                         utils::translate("common_minutes_lower_genitive"));
    }

    auto getCorrectSecondsNumeralForm(const std::uint32_t val) -> std::string
    {
        return transform(val,
                         utils::translate("common_second_lower"),
                         utils::translate("common_seconds_lower"),
                         utils::translate("common_seconds_lower_genitive"));
    }
} // namespace utils::language

M products/BellHybrid/apps/common/src/models/SettingsModel.cpp => products/BellHybrid/apps/common/src/models/SettingsModel.cpp +3 -0
@@ 3,6 3,7 @@

#include <models/SettingsModel.hpp>
#include <utf8/UTF8.hpp>
#include <widgets/list_items/Fraction.hpp>
namespace gui
{
    template <class ValueType> SettingsModel<ValueType>::SettingsModel(sys::Service *app)


@@ 12,7 13,9 @@ namespace gui

    template class SettingsModel<bool>;
    template class SettingsModel<std::uint8_t>;
    template class SettingsModel<std::uint32_t>;
    template class SettingsModel<std::string>;
    template class SettingsModel<UTF8>;
    template class SettingsModel<time_t>;
    template class SettingsModel<app::list_items::FractionData>;
} // namespace gui

M products/BellHybrid/apps/common/src/widgets/ListItems.cpp => products/BellHybrid/apps/common/src/widgets/ListItems.cpp +5 -146
@@ 12,17 12,17 @@ namespace gui
        : BellSideListItemWithCallbacks(topDescription), onStr{utils::translate("app_settings_toggle_on")},
          offStr{utils::translate("app_settings_toggle_off")}
    {
        spinner = new UTF8Spinner(UTF8Spinner::Range{offStr, onStr}, Boundaries::Fixed);
        spinner = new StringSpinner(StringSpinner::range{offStr, onStr}, Boundaries::Fixed);
        spinner->setMaximumSize(::style::bell_base_layout::w, ::style::bell_base_layout::h);
        spinner->setFont(bell_style::font);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        spinner->setFocusEdges(RectangleEdge::None);
        spinner->setCurrentValue(onStr);
        spinner->set_value(onStr);
        body->getCenterBox()->addWidget(spinner);

        getValue = [&model, this]() { model.setValue(isActive()); };
        setValue = [&model, this]() {
            spinner->setCurrentValue(model.getValue() ? onStr : offStr);
            spinner->set_value(model.getValue() ? onStr : offStr);
            setArrowsVisibility();
        };
        inputCallback = [this](Item &, const InputEvent &event) {


@@ 34,152 34,11 @@ namespace gui

    bool OnOffListItem::isActive() const
    {
        return spinner->getCurrentValue() == onStr;
        return spinner->value() == onStr;
    }
    void OnOffListItem::setArrowsVisibility()
    {
        const auto selectedVal = spinner->getCurrentValue();
        body->setMinMaxArrowsVisibility(selectedVal == offStr, selectedVal == onStr);
    }

    NumListItem::NumListItem(AbstractSettingsModel<std::uint8_t> &model,
                             UIntegerSpinner::Range range,
                             const std::string &topDescription,
                             const std::string &bottomDescription)
        : BellSideListItemWithCallbacks(topDescription)
    {
        spinner = new UIntegerSpinner(range, Boundaries::Fixed);
        spinner->setMaximumSize(::style::bell_base_layout::w, ::style::bell_base_layout::h);
        spinner->setFont(bell_style::font);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        spinner->setFocusEdges(RectangleEdge::None);
        body->getCenterBox()->addWidget(spinner);

        setupBottomDescription(bottomDescription);

        getValue = [&model, this]() { model.setValue(spinner->getCurrentValue()); };
        setValue = [&model, this, range]() {
            spinner->setCurrentValue(model.getValue());
            setArrowsVisibility(range);
        };

        inputCallback = [&, range](Item &item, const InputEvent &event) {
            const auto result = OnInputCallback(event);
            setArrowsVisibility(range);
            return result;
        };
    }
    void NumListItem::setOnValueChanged(std::function<void(const UIntegerSpinner::Type &)> &&cb)
    {
        spinner->onValueChanged = cb;
    }
    UIntegerSpinner::Type NumListItem::getCurrentValue()
    {
        return spinner->getCurrentValue();
    }

    void NumListItem::setArrowsVisibility(UIntegerSpinner::Range range)
    {
        const auto selectedVal = spinner->getCurrentValue();
        body->setMinMaxArrowsVisibility(selectedVal == range.min, selectedVal == range.max);
    }

    NumWithStringListItem::NumWithStringListItem(AbstractSettingsModel<std::uint8_t> &model,
                                                 NumWithStringSpinner::Range range,
                                                 const std::string &topDescription,
                                                 const std::string &bottomDescription)
        : BellSideListItemWithCallbacks(topDescription), minStr{utils::translate("common_minute_short")}
    {

        spinner = new NumWithStringSpinner(range, Boundaries::Fixed);
        spinner->setMaximumSize(::style::bell_base_layout::w, ::style::bell_base_layout::h);
        spinner->setFont(bell_style::font);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        spinner->setFocusEdges(RectangleEdge::None);
        body->getCenterBox()->addWidget(spinner);

        setupBottomDescription(bottomDescription);

        inputCallback = [&, range](Item &item, const InputEvent &event) {
            const auto result = OnInputCallback(event);
            bottomText->setVisible(spinner->getCurrentValue().getValue().has_value());
            setArrowsVisibility();
            return result;
        };

        focusChangedCallback = [&](Item &) {
            bottomText->setVisible(spinner->getCurrentValue().getValue().has_value());
            OnFocusChangedCallback();
            return true;
        };

        getValue = [&model, this]() {
            const auto val = spinner->getCurrentValue().getValue();
            model.setValue(not val ? 0 : *val);
        };
        setValue = [&model, this]() {
            const auto modelValue = model.getValue();
            if (modelValue > 0) {
                spinner->setCurrentValue(Value{modelValue, minStr});
            }
            else {
                spinner->setCurrentValue(Value{minStr});
            }
            setArrowsVisibility();
        };
    }

    bool NumWithStringListItem::isOff() const
    {
        return not spinner->getCurrentValue().getValue().has_value();
    }

    void NumWithStringListItem::setArrowsVisibility()
    {
        body->setMinMaxArrowsVisibility(spinner->isAtMin(), spinner->isAtMax());
    }
    NumWithString<uint32_t, UTF8> NumWithStringListItem::getCurrentValue()
    {
        return spinner->getCurrentValue();
    }

    UTF8ListItem::UTF8ListItem(AbstractSettingsModel<UTF8> &model,
                               UTF8Spinner::Range range,
                               const std::string &topDescription)
        : BellSideListItemWithCallbacks(topDescription)
    {
        spinner = new UTF8Spinner(range, Boundaries::Fixed);
        spinner->setMaximumSize(::style::bell_base_layout::w, ::style::bell_base_layout::h);
        spinner->setFont(bell_style::font_center);
        spinner->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        spinner->setFocusEdges(RectangleEdge::None);
        body->getCenterBox()->addWidget(spinner);

        getValue = [&model, this]() { model.setValue(spinner->getCurrentValue()); };
        setValue = [&model, this, range]() {
            spinner->setCurrentValue(model.getValue());
            setArrowsVisibility(range);
        };
        inputCallback = [&, range](Item &item, const InputEvent &event) {
            const auto result = OnInputCallback(event);
            setArrowsVisibility(range);
            return result;
        };
    }

    void UTF8ListItem::setOnValueChanged(std::function<void(const UTF8 &)> &&cb)
    {
        spinner->onValueChanged = cb;
    }

    UTF8Spinner::Type UTF8ListItem::getCurrentValue()
    {
        return spinner->getCurrentValue();
    }
    void UTF8ListItem::setArrowsVisibility(const UTF8Spinner::Range &range)
    {
        const auto selectedVal = spinner->getCurrentValue();
        body->setMinMaxArrowsVisibility(selectedVal == range.front(), selectedVal == range.back());
        body->setMinMaxArrowsVisibility(spinner->is_min(), spinner->is_max());
    }

    TimeListItem::TimeListItem(AbstractSettingsModel<time_t> &model,

M products/BellHybrid/apps/common/src/widgets/TimeSetSpinnerVertical.cpp => products/BellHybrid/apps/common/src/widgets/TimeSetSpinnerVertical.cpp +23 -21
@@ 29,25 29,25 @@ namespace gui
        setEdges(RectangleEdge::None);

        hour =
            new UIntegerSpinnerFixed(UIntegerSpinnerFixed::Range{hourMin, hourMax, hourStep}, Boundaries::Continuous);
            new UIntegerSpinnerFixed(UIntegerSpinnerFixed::range{hourMin, hourMax, hourStep}, Boundaries::Continuous);
        updateFont(hour, fontName);

        hour->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        hour->setEdges(RectangleEdge::None);
        hour->setPenFocusWidth(style::time_set_spinner_vertical::focus::size);
        hour->setCurrentValue(0);
        hour->set_value(0);
        hour->setMargins(getHourMargins(fontName));

        addWidget(hour);

        minute = new UIntegerSpinnerFixed(UIntegerSpinnerFixed::Range{minuteMin, minuteMax, minuteStep},
        minute = new UIntegerSpinnerFixed(UIntegerSpinnerFixed::range{minuteMin, minuteMax, minuteStep},
                                          Boundaries::Continuous);
        updateFont(minute, fontName);
        minute->setPenFocusWidth(style::time_set_spinner_vertical::focus::size);

        minute->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));
        minute->setEdges(RectangleEdge::None);
        minute->setCurrentValue(0);
        minute->set_value(0);
        addWidget(minute);

        resizeItems();


@@ 61,22 61,22 @@ namespace gui

        switch (newFormat) {
        case utils::time::Locale::TimeFormat::FormatTime12H: {
            auto hours = std::chrono::hours(hour->getCurrentValue());
            hour->setRange(UIntegerSpinnerFixed::Range{
            auto hours = std::chrono::hours(hour->value());
            hour->set_range(UIntegerSpinnerFixed::range{
                time::Locale::min_hour_12H_mode, time::Locale::max_hour_12H_mode, hourStep});

            if (timeFormat != newFormat) {
                hour->setCurrentValue(date::make12(hours).count());
                hour->set_value(date::make12(hours).count());
            }

        } break;
        case utils::time::Locale::TimeFormat::FormatTime24H: {
            auto hours = std::chrono::hours(hour->getCurrentValue());
            hour->setRange(UIntegerSpinnerFixed::Range{
            auto hours = std::chrono::hours(hour->value());
            hour->set_range(UIntegerSpinnerFixed::range{
                time::Locale::min_hour_24H_mode, time::Locale::max_hour_24H_mode, hourStep});

            if (newFormat != timeFormat) {
                hour->setCurrentValue(date::make24(hours, isPM()).count());
                hour->set_value(date::make24(hours, isPM()).count());
            }
        } break;
        default:


@@ 88,22 88,22 @@ namespace gui

    auto TimeSetSpinnerVertical::setMinute(int value) noexcept -> void
    {
        minute->setCurrentValue(value);
        minute->set_value(value);
    }

    auto TimeSetSpinnerVertical::getHour() const noexcept -> int
    {
        return hour->getCurrentValue();
        return hour->value();
    }

    auto TimeSetSpinnerVertical::getMinute() const noexcept -> int
    {
        return minute->getCurrentValue();
        return minute->value();
    }

    auto TimeSetSpinnerVertical::setHour(int value) noexcept -> void
    {
        hour->setCurrentValue(value);
        hour->set_value(value);
    }

    auto TimeSetSpinnerVertical::updateFont(TextFixedSize *elem, const std::string &fontName) noexcept -> void


@@ 148,15 148,17 @@ namespace gui
        const auto t = std::localtime(&time);

        if (timeFormat == Locale::TimeFormat::FormatTime24H) {
            hour->setCurrentValue(t->tm_hour);
            minute->setCurrentValue(t->tm_min);
            hour->set_value(t->tm_hour);
            minute->set_value(t->tm_min);
        }
        else {
            const auto hours   = std::chrono::hours{t->tm_hour};
            const auto time12H = date::make12(hours);
            hour->setCurrentValue(time12H.count());
            minute->setCurrentValue(t->tm_min);
            hour->set_value(time12H.count());
            minute->set_value(t->tm_min);
        }

        handleContentChanged();
    }

    auto TimeSetSpinnerVertical::getTime() const noexcept -> std::time_t


@@ 165,12 167,12 @@ namespace gui
        const auto now     = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
        const auto newTime = std::localtime(&now);
        if (timeFormat == Locale::TimeFormat::FormatTime24H) {
            newTime->tm_hour = hour->getCurrentValue();
            newTime->tm_hour = hour->value();
        }
        else {
            newTime->tm_hour = date::make24(std::chrono::hours{hour->getCurrentValue()}, isPM()).count();
            newTime->tm_hour = date::make24(std::chrono::hours{hour->value()}, isPM()).count();
        }
        newTime->tm_min = minute->getCurrentValue();
        newTime->tm_min = minute->value();

        return std::mktime(newTime);
    }