mirror of
https://github.com/mudita/MuditaOS.git
synced 2026-06-13 02:55:09 -04:00
Updated AlarmOptionsItem to use UTF8Spinner. Created specialized widgets to cover options sets. Updated GenericSpinner to handle Pure navigation and content swap. Updated Alarm RRule code to work with Custom Days selection and new widgets. Added Tests. General GUI stylistic fixes. Increased app Alarm Clock and service Time stack sizes.
627 lines
20 KiB
C++
627 lines
20 KiB
C++
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
|
|
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
|
|
|
|
#include "BoxLayout.hpp"
|
|
#include "BoxLayoutSizeStore.hpp"
|
|
#include <InputEvent.hpp>
|
|
#include <Label.hpp>
|
|
#include <log.hpp>
|
|
#include "assert.h"
|
|
|
|
namespace gui
|
|
{
|
|
BoxLayout::BoxLayout(Item *parent, const uint32_t &x, const uint32_t &y, const uint32_t &w, const uint32_t &h)
|
|
: Rect(parent, x, y, w, h)
|
|
{
|
|
alignment = gui::Alignment(Alignment::Horizontal::None, Alignment::Vertical::None);
|
|
sizeStore = std::make_unique<BoxLayoutSizeStore>();
|
|
}
|
|
|
|
BoxLayout::BoxLayout() : BoxLayout(nullptr, 0, 0, 0, 0)
|
|
{}
|
|
|
|
bool BoxLayout::onInput(const InputEvent &inputEvent)
|
|
{
|
|
if (inputCallback && inputCallback(*this, inputEvent)) {
|
|
return true;
|
|
}
|
|
if (focusItem && focusItem->onInput(inputEvent)) {
|
|
return true;
|
|
}
|
|
if (handleNavigation(inputEvent)) {
|
|
return true;
|
|
}
|
|
if (borderCallback && isInputNavigation(inputEvent) && borderCallback(inputEvent)) {
|
|
outOfDrawAreaItems.clear();
|
|
return true;
|
|
}
|
|
// let item logic rule it
|
|
return false;
|
|
}
|
|
|
|
bool BoxLayout::onFocus(bool state)
|
|
{
|
|
if (state)
|
|
this->setVisible(state);
|
|
else
|
|
this->setFocusItem(nullptr);
|
|
this->setNavigation();
|
|
if (this->focusChangedCallback && state != focus) {
|
|
this->focusChangedCallback(*this);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void BoxLayout::resizeItems()
|
|
{}
|
|
|
|
void BoxLayout::setAlignment(const Alignment &value)
|
|
{
|
|
if (alignment != value) {
|
|
alignment = value;
|
|
resizeItems();
|
|
}
|
|
}
|
|
|
|
void BoxLayout::addWidget(Item *item)
|
|
{
|
|
Rect::addWidget(item);
|
|
resizeItems();
|
|
}
|
|
|
|
template <Axis axis> void BoxLayout::addWidget(Item *item)
|
|
{
|
|
Rect::addWidget(item);
|
|
resizeItems<axis>();
|
|
}
|
|
|
|
bool BoxLayout::removeWidget(Item *item)
|
|
{
|
|
bool ret = Rect::removeWidget(item);
|
|
|
|
outOfDrawAreaItems.remove(item);
|
|
resizeItems();
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool BoxLayout::erase(Item *item)
|
|
{
|
|
auto ret = Item::erase(item);
|
|
|
|
outOfDrawAreaItems.remove(item);
|
|
resizeItems();
|
|
|
|
return ret;
|
|
}
|
|
|
|
void BoxLayout::erase()
|
|
{
|
|
Item::erase();
|
|
}
|
|
|
|
bool BoxLayout::empty() const noexcept
|
|
{
|
|
return children.empty();
|
|
}
|
|
|
|
void BoxLayout::setVisible(bool value, bool previous)
|
|
{
|
|
visible = value; // maybe use parent setVisible(...)? would be better but which one?
|
|
if (value == true) {
|
|
resizeItems(); // move items in box in proper places
|
|
setNavigation(); // set navigation through kids -> TODO handle out of last/first to parent
|
|
if (children.size()) { // set first visible kid as focused item - TODO should check for actionItems too...
|
|
/// this if back / front is crappy :|
|
|
if (previous) {
|
|
for (auto el = children.rbegin(); el != children.rend(); ++el) {
|
|
if ((*el)->isActive()) {
|
|
setFocusItem(*el);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (auto &el : children) {
|
|
if (el->isActive()) {
|
|
setFocusItem(el);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void BoxLayout::setVisible(bool value)
|
|
{
|
|
setVisible(value, false);
|
|
}
|
|
|
|
unsigned int BoxLayout::getVisibleChildrenCount()
|
|
{
|
|
assert(children.size() >= outOfDrawAreaItems.size());
|
|
return children.size() - outOfDrawAreaItems.size();
|
|
}
|
|
|
|
void BoxLayout::setReverseOrder(bool value)
|
|
{
|
|
reverseOrder = value;
|
|
}
|
|
|
|
bool BoxLayout::getReverseOrder()
|
|
{
|
|
return reverseOrder;
|
|
}
|
|
|
|
Length BoxLayout::getPrimarySize()
|
|
{
|
|
if (type == ItemType::HBOX) {
|
|
return getSize(Axis::X);
|
|
}
|
|
else if (type == ItemType::VBOX) {
|
|
return getSize(Axis::Y);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
Length BoxLayout::getPrimarySizeLeft()
|
|
{
|
|
if (type == ItemType::HBOX) {
|
|
return sizeLeft<Axis::X>(this, Area::Normal);
|
|
}
|
|
else if (type == ItemType::VBOX) {
|
|
return sizeLeft<Axis::Y>(this, Area::Normal);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void BoxLayout::addToOutOfDrawAreaList(Item *it)
|
|
{
|
|
if (it->visible) {
|
|
outOfDrawAreaItems.push_back(it);
|
|
it->visible = false;
|
|
}
|
|
}
|
|
|
|
void BoxLayout::addFromOutOfDrawAreaList()
|
|
{
|
|
if (children.size() != 0) {
|
|
for (auto it : outOfDrawAreaItems) {
|
|
it->setVisible(true);
|
|
it->setFocusItem(nullptr);
|
|
}
|
|
}
|
|
|
|
outOfDrawAreaItems.clear();
|
|
resizeItems();
|
|
}
|
|
|
|
// space left distposition `first is better` tactics
|
|
// there could be other i.e. socialism: each element take in equal part up to it's max size
|
|
// not needed now == not implemented
|
|
template <Axis axis> void BoxLayout::resizeItems()
|
|
{
|
|
Position startingPosition = reverseOrder ? this->area().size(axis) : 0;
|
|
Position leftPosition = this->getSize(axis);
|
|
Length toSplit = sizeLeft<axis>(this);
|
|
|
|
for (auto &el : children) {
|
|
|
|
if (!el->visible)
|
|
continue;
|
|
|
|
auto axisItemPosition = 0;
|
|
auto orthogonalItemPosition = 0;
|
|
auto axisItemSize = 0;
|
|
auto calculatedResize = 0;
|
|
auto orthogonalItemSize = 0;
|
|
|
|
// 1. Calculate element axis resize.
|
|
calculatedResize = calculateElemResize<axis>(el, toSplit);
|
|
|
|
// 2. Check if new size in axis can be applied and set it or use element minimal size in axis.
|
|
axisItemSize = calculateElemAxisSize<axis>(el, calculatedResize, toSplit);
|
|
|
|
// 3. Calculate orthogonal axis size.
|
|
orthogonalItemSize = calculateElemOrtAxisSize<axis>(el);
|
|
|
|
// 4. Calculate element position in axis and apply in axis alignment.
|
|
axisItemPosition = calculateElemAxisPosition<axis>(el, axisItemSize, startingPosition, leftPosition);
|
|
|
|
// 5. Calculate element orthogonal axis position based on Layout alignment or on element alignment.
|
|
orthogonalItemPosition = calculateElemOrtAxisPosition<axis>(el, orthogonalItemSize);
|
|
|
|
// 6. If element still visible (not added to outOfDrawAreaList) set it Area with calculated values.
|
|
if (el->visible)
|
|
el->setAreaInAxis(axis, axisItemPosition, orthogonalItemPosition, axisItemSize, orthogonalItemSize);
|
|
}
|
|
|
|
Rect::updateDrawArea();
|
|
}
|
|
|
|
template <Axis axis> Length BoxLayout::calculateElemResize(Item *el, Length &toSplit)
|
|
{
|
|
auto grantedSize = sizeStore->get(el);
|
|
Length calculatedResize = 0;
|
|
|
|
// Check if element has stored requested size in axis.
|
|
if (!grantedSize.isZero()) {
|
|
calculatedResize = grantedSize.get(axis) < el->area(Area::Min).size(axis)
|
|
? 0
|
|
: grantedSize.get(axis) - el->area(Area::Min).size(axis);
|
|
|
|
// If requested size bigger than left size in layout push out last visible element in layout into
|
|
// outOfDrawAreaList.
|
|
if (sizeLeft<axis>(this) < calculatedResize) {
|
|
addToOutOfDrawAreaList(getLastVisibleElement());
|
|
toSplit = sizeLeft<axis>(this);
|
|
}
|
|
}
|
|
else {
|
|
// If not calculate possible size in axis from min-max difference.
|
|
calculatedResize = el->area(Area::Max).size(axis) < el->area(Area::Min).size(axis)
|
|
? 0
|
|
: el->area(Area::Max).size(axis) - el->area(Area::Min).size(axis);
|
|
}
|
|
|
|
return calculatedResize;
|
|
}
|
|
|
|
template <Axis axis> Length BoxLayout::calculateElemAxisSize(Item *el, Length calculatedResize, Length &toSplit)
|
|
{
|
|
Length axisItemSize = 0;
|
|
|
|
if (toSplit > 0 && calculatedResize > 0) {
|
|
axisItemSize = el->area(Area::Min).size(axis) + std::min(calculatedResize, toSplit);
|
|
toSplit -= std::min(calculatedResize, toSplit);
|
|
}
|
|
else {
|
|
axisItemSize = el->area(Area::Min).size(axis);
|
|
}
|
|
|
|
return axisItemSize;
|
|
}
|
|
|
|
template <Axis axis> Length BoxLayout::calculateElemOrtAxisSize(Item *el)
|
|
{
|
|
// Get maximum size that element in orthogonal axis can occupy in current layout size.
|
|
Length maxOrthogonalItemInParentSize =
|
|
static_cast<Position>(this->area(Area::Normal).size(orthogonal(axis))) <=
|
|
el->getMargins().getSumInAxis(orthogonal(axis))
|
|
? 0
|
|
: this->area(Area::Normal).size(orthogonal(axis)) - el->getMargins().getSumInAxis(orthogonal(axis));
|
|
|
|
// Get maximum size of element in orthogonal axis based on its min-max.
|
|
Length maxOrthogonalItemSize =
|
|
el->area(Area::Max).size(orthogonal(axis)) > el->area(Area::Min).size(orthogonal(axis))
|
|
? el->area(Area::Max).size(orthogonal(axis))
|
|
: el->area(Area::Min).size(orthogonal(axis));
|
|
|
|
// Return minimal from both max sizes.
|
|
return std::min(maxOrthogonalItemInParentSize, maxOrthogonalItemSize);
|
|
}
|
|
|
|
template <Axis axis>
|
|
Position BoxLayout::calculateElemAxisPosition(Item *el,
|
|
Length axisItemSize,
|
|
Position &startingPosition,
|
|
Position &leftPosition)
|
|
{
|
|
auto axisItemPosition = 0;
|
|
|
|
// Check if elements in axis can fit with margins in layout free space.
|
|
if (((Position)(axisItemSize + el->getMargins().getSumInAxis(axis))) <= leftPosition) {
|
|
|
|
if (reverseOrder) {
|
|
startingPosition -= el->getMargins().getMarginInAxis(axis, MarginInAxis::Second);
|
|
startingPosition -= axisItemSize;
|
|
axisItemPosition = startingPosition;
|
|
startingPosition -= el->getMargins().getMarginInAxis(axis, MarginInAxis::First);
|
|
}
|
|
|
|
if (!reverseOrder) {
|
|
startingPosition += el->getMargins().getMarginInAxis(axis, MarginInAxis::First);
|
|
axisItemPosition = startingPosition;
|
|
startingPosition += axisItemSize;
|
|
startingPosition += el->getMargins().getMarginInAxis(axis, MarginInAxis::Second);
|
|
}
|
|
|
|
leftPosition -= axisItemSize + el->getMargins().getSumInAxis(axis);
|
|
|
|
// Recalculate element axis position if axis alignment provided.
|
|
axisItemPosition = getAxisAlignmentValue<axis>(axisItemPosition, axisItemSize, el);
|
|
}
|
|
else {
|
|
// If not add it to outOfDrawAreaList.
|
|
addToOutOfDrawAreaList(el);
|
|
}
|
|
|
|
return axisItemPosition;
|
|
}
|
|
|
|
template <Axis axis> Position BoxLayout::calculateElemOrtAxisPosition(Item *el, Length orthogonalItemSize)
|
|
{
|
|
return el->getAxisAlignmentValue(orthogonal(axis), orthogonalItemSize);
|
|
}
|
|
|
|
template <Axis axis> Position BoxLayout::getAxisAlignmentValue(Position calcPos, Length calcSize, Item *el)
|
|
{
|
|
auto offset = sizeLeftWithoutElem<axis>(this, el, Area::Normal) <= calcSize
|
|
? 0
|
|
: sizeLeftWithoutElem<axis>(this, el, Area::Normal) - calcSize;
|
|
|
|
switch (getAlignment(axis).vertical) {
|
|
case gui::Alignment::Vertical::Top:
|
|
if (reverseOrder) {
|
|
return calcPos + el->getMargins().getSumInAxis(axis) - offset;
|
|
}
|
|
break;
|
|
case gui::Alignment::Vertical::Center:
|
|
if (reverseOrder) {
|
|
return calcPos - ((offset - el->getMargins().getSumInAxis(axis)) / 2);
|
|
}
|
|
else {
|
|
return calcPos + ((offset - el->getMargins().getSumInAxis(axis)) / 2);
|
|
}
|
|
break;
|
|
case gui::Alignment::Vertical::Bottom:
|
|
if (!reverseOrder) {
|
|
return calcPos - el->getMargins().getSumInAxis(axis) + offset;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
switch (getAlignment(axis).horizontal) {
|
|
case gui::Alignment::Horizontal::Left:
|
|
if (reverseOrder) {
|
|
return calcPos + el->getMargins().getSumInAxis(axis) - offset;
|
|
}
|
|
break;
|
|
case gui::Alignment::Horizontal::Center:
|
|
if (reverseOrder) {
|
|
return calcPos - ((offset - el->getMargins().getSumInAxis(axis)) / 2);
|
|
}
|
|
else {
|
|
return calcPos + ((offset - el->getMargins().getSumInAxis(axis)) / 2);
|
|
}
|
|
break;
|
|
case gui::Alignment::Horizontal::Right:
|
|
if (!reverseOrder) {
|
|
return calcPos - el->getMargins().getSumInAxis(axis) + offset;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return calcPos;
|
|
}
|
|
|
|
std::list<Item *>::iterator BoxLayout::nextNavigationItem(std::list<Item *>::iterator from)
|
|
{
|
|
return std::find_if(from, this->children.end(), [](auto &el) -> bool {
|
|
if (el->isActive()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
std::list<Item *>::iterator BoxLayout::getNavigationFocusedItem()
|
|
{
|
|
return std::find(this->children.begin(), this->children.end(), this->getFocusItem());
|
|
}
|
|
|
|
void BoxLayout::setNavigation()
|
|
{
|
|
auto previous = nextNavigationItem(children.begin()), next = children.end();
|
|
if (type == ItemType::VBOX) {
|
|
while ((previous != children.end()) &&
|
|
((next = nextNavigationItem(std::next(previous))) != std::end(children))) {
|
|
(*previous)->setNavigationItem(reverseOrder ? NavigationDirection::UP : NavigationDirection::DOWN,
|
|
*next);
|
|
(*next)->setNavigationItem(reverseOrder ? NavigationDirection::DOWN : NavigationDirection::UP,
|
|
*previous);
|
|
previous = next;
|
|
}
|
|
|
|
if (previous != children.end()) {
|
|
if ((*previous) != nullptr) {
|
|
(*previous)->setNavigationItem(reverseOrder ? NavigationDirection::UP : NavigationDirection::DOWN,
|
|
nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (type == ItemType::HBOX) {
|
|
while ((previous != children.end()) &&
|
|
((next = nextNavigationItem(std::next(previous))) != std::end(children))) {
|
|
(*previous)->setNavigationItem(reverseOrder ? NavigationDirection::LEFT : NavigationDirection::RIGHT,
|
|
*next);
|
|
(*next)->setNavigationItem(reverseOrder ? NavigationDirection::RIGHT : NavigationDirection::LEFT,
|
|
*previous);
|
|
previous = next;
|
|
}
|
|
|
|
if (previous != children.end()) {
|
|
if ((*previous) != nullptr) {
|
|
(*previous)->setNavigationItem(
|
|
reverseOrder ? NavigationDirection::LEFT : NavigationDirection::RIGHT, nullptr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template <Axis axis>
|
|
auto BoxLayout::handleRequestResize(const Item *child, Length request_w, Length request_h) -> Size
|
|
{
|
|
if (parent != nullptr) {
|
|
auto [w, h] = requestSize(request_w, request_h);
|
|
request_w = std::min(w, (Length)request_w);
|
|
request_h = std::min(h, (Length)request_h);
|
|
}
|
|
|
|
auto el = std::find(children.begin(), children.end(), child);
|
|
if (el == std::end(children)) {
|
|
return {0, 0};
|
|
}
|
|
|
|
Size granted = {std::min((*el)->area(Area::Max).w, request_w), std::min((*el)->area(Area::Max).h, request_h)};
|
|
|
|
// Currently not used option for Layouts that don't push out objects.
|
|
if (!sizeStore->getReleaseSpaceFlag()) {
|
|
if ((granted.get(axis) + (*el)->getMargins().getSumInAxis(axis)) >=
|
|
sizeLeftWithoutElem<axis>(this, *el, Area::Min)) {
|
|
|
|
granted = Size((*el)->area(Area::Normal).w, (*el)->area(Area::Normal).h);
|
|
}
|
|
}
|
|
|
|
// If granted size decreased check if pushed out elements can fit
|
|
if (sizeStore->isSizeSmaller(*el, granted, axis)) {
|
|
addFromOutOfDrawAreaList();
|
|
}
|
|
|
|
sizeStore->store(*el, granted);
|
|
resizeItems(); // vs mark dirty
|
|
setNavigation();
|
|
|
|
if (parentOnRequestedResizeCallback != nullptr) {
|
|
parentOnRequestedResizeCallback();
|
|
}
|
|
|
|
return granted;
|
|
}
|
|
|
|
bool BoxLayout::setFocusOnElement(unsigned int elementNumber)
|
|
{
|
|
unsigned int i = 0;
|
|
bool success = false;
|
|
|
|
for (auto child : children) {
|
|
if (child->isActive()) {
|
|
|
|
if (elementNumber == i) {
|
|
child->setFocus(true);
|
|
focusItem = child;
|
|
success = true;
|
|
}
|
|
else {
|
|
child->setFocus(false);
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void BoxLayout::setFocusOnLastElement()
|
|
{
|
|
auto last = true;
|
|
for (auto child = children.rbegin(); child != children.rend(); child++) {
|
|
|
|
if ((*child)->isActive() && last) {
|
|
(*child)->setFocus(true);
|
|
focusItem = (*child);
|
|
last = false;
|
|
}
|
|
else {
|
|
(*child)->setFocus(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
unsigned int BoxLayout::getFocusItemIndex() const
|
|
{
|
|
auto index = 0;
|
|
auto focusItem = getFocusItem();
|
|
|
|
for (auto child : children) {
|
|
if (child == focusItem) {
|
|
break;
|
|
}
|
|
if (child->isActive()) {
|
|
index++;
|
|
}
|
|
}
|
|
return index;
|
|
}
|
|
|
|
Item *BoxLayout::getLastVisibleElement()
|
|
{
|
|
for (auto child = children.rbegin(); child != children.rend(); child++) {
|
|
if ((*child)->visible) {
|
|
return (*child);
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
auto BoxLayout::onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool
|
|
{
|
|
addFromOutOfDrawAreaList();
|
|
resizeItems();
|
|
setNavigation();
|
|
|
|
return Item::onDimensionChanged(oldDim, newDim);
|
|
}
|
|
|
|
HBox::HBox() : BoxLayout()
|
|
{
|
|
type = ItemType::HBOX;
|
|
}
|
|
|
|
HBox::HBox(Item *parent, const uint32_t &x, const uint32_t &y, const uint32_t &w, const uint32_t &h)
|
|
: BoxLayout(parent, x, y, w, h)
|
|
{
|
|
type = ItemType::HBOX;
|
|
}
|
|
|
|
void HBox::resizeItems()
|
|
{
|
|
BoxLayout::resizeItems<Axis::X>();
|
|
}
|
|
|
|
void HBox::addWidget(Item *item)
|
|
{
|
|
BoxLayout::addWidget<Axis::X>(item);
|
|
}
|
|
|
|
auto HBox::handleRequestResize(const Item *child, Length request_w, Length request_h) -> Size
|
|
{
|
|
return BoxLayout::handleRequestResize<Axis::X>(child, request_w, request_h);
|
|
}
|
|
|
|
VBox::VBox() : BoxLayout()
|
|
{
|
|
type = ItemType::VBOX;
|
|
}
|
|
|
|
VBox::VBox(Item *parent, const uint32_t &x, const uint32_t &y, const uint32_t &w, const uint32_t &h)
|
|
: BoxLayout(parent, x, y, w, h)
|
|
{
|
|
type = ItemType::VBOX;
|
|
}
|
|
|
|
void VBox::resizeItems()
|
|
{
|
|
BoxLayout::resizeItems<Axis::Y>();
|
|
}
|
|
|
|
void VBox::addWidget(Item *item)
|
|
{
|
|
BoxLayout::addWidget<Axis::Y>(item);
|
|
}
|
|
|
|
auto VBox::handleRequestResize(const Item *child, Length request_w, Length request_h) -> Size
|
|
{
|
|
return BoxLayout::handleRequestResize<Axis::Y>(child, request_w, request_h);
|
|
}
|
|
} /* namespace gui */
|