M changelog.md => changelog.md +1 -0
@@ 19,6 19,7 @@
* `[system]` Timer API - linked to timer, same for Services and Applications. Updated docs
* `[system]` Removed `using std` and `using cpp_freertos` from commonly used headers
+* `[messages]` Refactored messages SMS thread window to use ListView.
### Fixed
M module-apps/DatabaseModel.hpp => module-apps/DatabaseModel.hpp +1 -1
@@ 16,7 16,7 @@ namespace app
protected:
/// Pointer to application that owns the model
Application *application = nullptr;
- uint32_t recordsCount;
+ uint32_t recordsCount = 0;
std::vector<std::shared_ptr<T>> records;
uint32_t modelIndex = 0;
M module-apps/application-messages/CMakeLists.txt => module-apps/application-messages/CMakeLists.txt +4 -0
@@ 18,12 18,14 @@ target_sources( ${PROJECT_NAME}
"widgets/SMSTemplateModel.cpp"
"widgets/SMSTemplateItem.cpp"
"widgets/SMSInputWidget.cpp"
+ "widgets/SMSOutputWidget.cpp"
"widgets/SearchResultsItem.cpp"
"widgets/BaseThreadItem.cpp"
"models/BaseThreadsRecordModel.cpp"
"models/ThreadsModel.cpp"
"models/ThreadsSearchResultsModel.cpp"
+ "models/SMSThreadModel.cpp"
"windows/MessagesMainWindow.cpp"
"windows/SMSThreadViewWindow.cpp"
@@ 40,7 42,9 @@ target_sources( ${PROJECT_NAME}
"ApplicationMessages.hpp"
"data/MessagesStyle.hpp"
"models/ThreadsModel.hpp"
+ "models/SMSThreadModel.hpp"
"widgets/ThreadItem.hpp"
"widgets/SMSInputWidget.hpp"
+ "widgets/SMSOutputWidget.hpp"
)
M module-apps/application-messages/data/MessagesStyle.hpp => module-apps/application-messages/data/MessagesStyle.hpp +38 -6
@@ 17,6 17,8 @@ namespace style
namespace threadItem
{
+ constexpr uint32_t sms_thread_item_h = 100;
+
constexpr uint32_t topMargin = 16;
constexpr uint32_t bottomMargin = 13;
@@ 52,14 54,44 @@ namespace style
namespace smsInput
{
- constexpr uint32_t min_h = 40;
- constexpr uint32_t default_input_w = 405;
- constexpr uint32_t default_input_h = 30;
- constexpr uint32_t bottom_padding = 5;
- constexpr uint32_t max_input_h = default_input_h * 4 + bottom_padding;
- constexpr uint32_t reply_bottom_margin = 5;
+ constexpr gui::Length min_h = 40;
+ constexpr gui::Length default_input_w = 395;
+ constexpr gui::Length default_input_h = 30;
+ constexpr gui::Length bottom_padding = 5;
+ constexpr gui::Length max_input_h = default_input_h * 4 + bottom_padding;
+ constexpr gui::Length reply_bottom_margin = 5;
+ constexpr gui::Length new_sms_vertical_spacer = 25;
} // namespace smsInput
+ namespace smsOutput
+ {
+ constexpr gui::Length sms_radius = 7;
+ constexpr gui::Length default_h = 30;
+ constexpr gui::Length sms_max_width = 320;
+ constexpr gui::Length sms_h_padding = 15;
+ constexpr gui::Length sms_h_big_padding = 25;
+ constexpr gui::Length sms_v_padding = 10;
+ constexpr gui::Length sms_vertical_spacer = 10;
+ constexpr gui::Length sms_error_icon_left_margin = 5;
+ constexpr gui::Length sms_error_icon_right_margin = 2;
+ const inline gui::Padding sms_left_bubble_padding = gui::Padding(smsOutput::sms_h_big_padding,
+ smsOutput::sms_v_padding,
+ smsOutput::sms_h_padding,
+ smsOutput::sms_v_padding);
+ const inline gui::Padding sms_right_bubble_padding = gui::Padding(smsOutput::sms_h_padding,
+ smsOutput::sms_v_padding,
+ smsOutput::sms_h_big_padding,
+ smsOutput::sms_v_padding);
+ } // namespace smsOutput
+
+ namespace smsList
+ {
+ constexpr uint32_t x = style::window::default_left_margin;
+ constexpr uint32_t y = style::header::height;
+ constexpr uint32_t h = style::window::default_body_height;
+ constexpr uint32_t w = style::listview::body_width_with_scroll;
+ } // namespace smsList
+
namespace templates
{
namespace list
A module-apps/application-messages/models/SMSThreadModel.cpp => module-apps/application-messages/models/SMSThreadModel.cpp +123 -0
@@ 0,0 1,123 @@
+#include <module-services/service-db/messages/QueryMessage.hpp>
+#include <module-services/service-db/api/DBServiceAPI.hpp>
+#include <module-db/queries/messages/sms/QuerySMSGetCountByThreadID.hpp>
+#include <module-db/queries/messages/sms/QuerySMSGetForList.hpp>
+
+#include <application-messages/widgets/SMSOutputWidget.hpp>
+#include <module-apps/application-messages/ApplicationMessages.hpp>
+#include "application-messages/data/MessagesStyle.hpp"
+#include "SMSThreadModel.hpp"
+#include "ListView.hpp"
+
+SMSThreadModel::SMSThreadModel(app::Application *app) : DatabaseModel(app)
+{
+ smsInput = new gui::SMSInputWidget(application);
+}
+
+SMSThreadModel::~SMSThreadModel()
+{
+ delete smsInput;
+}
+
+unsigned int SMSThreadModel::getMinimalItemHeight() const
+{
+ return style::messages::smsOutput::default_h;
+}
+
+gui::ListItem *SMSThreadModel::getItem(gui::Order order)
+{
+ std::shared_ptr<SMSRecord> sms = getRecord(order);
+
+ if (sms == nullptr) {
+ return nullptr;
+ }
+
+ // Small hack to trick current model logic -> adding empty row into query result for Input Widget
+ if (sms->type == SMSType::INPUT) {
+ addReturnNumber();
+ return smsInput;
+ }
+
+ return new gui::SMSOutputWidget(application, sms);
+}
+
+unsigned int SMSThreadModel::requestRecordsCount()
+{
+ return recordsCount;
+}
+
+void SMSThreadModel::requestRecords(uint32_t offset, uint32_t limit)
+{
+ auto query = std::make_unique<db::query::SMSGetForList>(smsThreadID, offset, limit);
+ query->setQueryListener(
+ db::QueryCallback::fromFunction([this](auto response) { return handleQueryResponse(response); }));
+ DBServiceAPI::GetQuery(application, db::Interface::Name::SMS, std::move(query));
+}
+
+bool SMSThreadModel::updateRecords(std::unique_ptr<std::vector<SMSRecord>> records)
+{
+ DatabaseModel::updateRecords(std::move(records));
+ list->onProviderDataUpdate();
+ return true;
+}
+
+auto SMSThreadModel::handleQueryResponse(db::QueryResult *queryResult) -> bool
+{
+ auto msgResponse = dynamic_cast<db::query::SMSGetForListResult *>(queryResult);
+ assert(msgResponse != nullptr);
+
+ auto records_data = msgResponse->getResults();
+
+ // If list record count has changed we need to rebuild list.
+ if (recordsCount != (msgResponse->getCount() + 1)) {
+ // Additional one element for SMSInputWidget.
+ recordsCount = msgResponse->getCount() + 1;
+ list->rebuildList(style::listview::RebuildType::Full, 0, true);
+ return false;
+ }
+
+ resetInputWidget();
+
+ if (msgResponse->getDraft().isValid()) {
+ smsInput->draft = msgResponse->getDraft().type == SMSType::DRAFT
+ ? std::optional<SMSRecord>{msgResponse->getDraft()}
+ : std::nullopt;
+ smsInput->displayDraftMessage();
+ }
+
+ auto records = std::make_unique<std::vector<SMSRecord>>(records_data.begin(), records_data.end());
+
+ return this->updateRecords(std::move(records));
+}
+
+void SMSThreadModel::addReturnNumber()
+{
+ if (number != nullptr) {
+ smsInput->number = std::move(number);
+ }
+
+ smsInput->activatedCallback = [this]([[maybe_unused]] gui::Item &item) {
+ auto app = dynamic_cast<app::ApplicationMessages *>(application);
+ assert(app != nullptr);
+ assert(smsInput->number != nullptr);
+ if (app->handleSendSmsFromThread(*smsInput->number, smsInput->inputText->getText())) {
+ LOG_ERROR("handleSendSmsFromThread failed");
+ }
+ smsInput->inputText->clear();
+ smsInput->clearDraftMessage();
+ return true;
+ };
+}
+
+void SMSThreadModel::handleDraftMessage()
+{
+ smsInput->handleDraftMessage();
+}
+
+void SMSThreadModel::resetInputWidget()
+{
+ smsInput->setFocus(false);
+ smsInput->setVisible(true);
+ smsInput->clearNavigationItem(gui::NavigationDirection::UP);
+ smsInput->clearNavigationItem(gui::NavigationDirection::DOWN);
+}
A module-apps/application-messages/models/SMSThreadModel.hpp => module-apps/application-messages/models/SMSThreadModel.hpp +30 -0
@@ 0,0 1,30 @@
+#pragma once
+
+#include "DatabaseModel.hpp"
+#include "Application.hpp"
+#include "ListItemProvider.hpp"
+#include "Interface/SMSRecord.hpp"
+#include <application-messages/widgets/SMSInputWidget.hpp>
+
+class SMSThreadModel : public app::DatabaseModel<SMSRecord>, public gui::ListItemProvider
+{
+ public:
+ unsigned int smsThreadID = 0;
+ gui::SMSInputWidget *smsInput = nullptr;
+ std::unique_ptr<utils::PhoneNumber::View> number;
+
+ SMSThreadModel(app::Application *app);
+ ~SMSThreadModel() override;
+
+ void addReturnNumber();
+ void handleDraftMessage();
+ void resetInputWidget();
+
+ auto handleQueryResponse(db::QueryResult *) -> bool;
+
+ unsigned int requestRecordsCount() override;
+ bool updateRecords(std::unique_ptr<std::vector<SMSRecord>> records) override;
+ void requestRecords(uint32_t offset, uint32_t limit) override;
+ unsigned int getMinimalItemHeight() const override;
+ gui::ListItem *getItem(gui::Order order) override;
+};
M module-apps/application-messages/models/ThreadsModel.cpp => module-apps/application-messages/models/ThreadsModel.cpp +2 -2
@@ 2,17 2,17 @@
#include "InputEvent.hpp"
#include "OptionWindow.hpp"
#include "application-messages/data/SMSdata.hpp"
+#include "application-messages/data/MessagesStyle.hpp"
#include "application-messages/widgets/ThreadItem.hpp"
#include "application-messages/windows/ThreadWindowOptions.hpp"
#include <module-services/service-db/api/DBServiceAPI.hpp>
-#include <module-services/service-db/messages/DBThreadMessage.hpp>
ThreadsModel::ThreadsModel(app::Application *app) : BaseThreadsRecordModel(app)
{}
auto ThreadsModel::getMinimalItemHeight() const -> unsigned int
{
- return style::window::messages::sms_thread_item_h;
+ return style::messages::threadItem::sms_thread_item_h;
}
auto ThreadsModel::getItem(gui::Order order) -> gui::ListItem *
M module-apps/application-messages/models/ThreadsSearchResultsModel.cpp => module-apps/application-messages/models/ThreadsSearchResultsModel.cpp +2 -1
@@ 6,6 6,7 @@
#include "service-db/api/DBServiceAPI.hpp"
#include <module-db/queries/messages/threads/QueryThreadsSearch.hpp>
#include <module-apps/application-messages/ApplicationMessages.hpp>
+#include "application-messages/data/MessagesStyle.hpp"
namespace gui::model
{
@@ 15,7 16,7 @@ namespace gui::model
auto ThreadsSearchResultsModel::getMinimalItemHeight() const -> unsigned int
{
- return style::window::messages::sms_thread_item_h;
+ return style::messages::threadItem::sms_thread_item_h;
}
auto ThreadsSearchResultsModel::getItem(gui::Order order) -> gui::ListItem *
M module-apps/application-messages/widgets/BaseThreadItem.cpp => module-apps/application-messages/widgets/BaseThreadItem.cpp +2 -2
@@ 8,8 8,8 @@ namespace gui
{
using namespace style;
setMargins(Margins(0, style::margins::small, 0, style::margins::small));
- setMinimumSize(window::default_body_width, style::window::messages::sms_thread_item_h);
- setMaximumSize(window::default_body_width, style::window::messages::sms_thread_item_h);
+ setMinimumSize(window::default_body_width, style::messages::threadItem::sms_thread_item_h);
+ setMaximumSize(window::default_body_width, style::messages::threadItem::sms_thread_item_h);
setRadius(0);
setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_BOTTOM | RectangleEdgeFlags::GUI_RECT_EDGE_TOP);
M module-apps/application-messages/widgets/SMSInputWidget.cpp => module-apps/application-messages/widgets/SMSInputWidget.cpp +104 -14
@@ 7,36 7,48 @@
#include <i18/i18.hpp>
#include <Font.hpp>
#include <utility>
+#include <algorithm>
#include "TextParse.hpp"
namespace gui
{
- SMSInputWidget::SMSInputWidget(Item *parent, app::Application *application) : HBox(parent, 0, 0, 0, 0)
+ SMSInputWidget::SMSInputWidget(app::Application *application) : application(application)
{
-
setMinimumSize(style::window::default_body_width, style::messages::smsInput::min_h);
- setMaximumHeight(style::messages::smsInput::max_input_h);
- setMargins(Margins(0, style::window::messages::new_sms_vertical_spacer, 0, 0));
- setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_BOTTOM);
+ setMargins(Margins(0, style::messages::smsInput::new_sms_vertical_spacer, 0, 0));
+ setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
+
+ body = new HBox(this, 0, 0, 0, 0);
+ body->setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_BOTTOM);
+ body->setMaximumSize(style::window::default_body_width, style::messages::smsInput::max_input_h);
- inputText = new gui::Text(this, 0, 0, 0, 0, "", ExpandMode::EXPAND_UP);
+ deleteByList = false;
+
+ inputText = new gui::Text(body, 0, 0, 0, 0, "", ExpandMode::EXPAND_UP);
+ inputText->setMaximumSize(style::messages::smsInput::default_input_w, style::messages::smsInput::max_input_h);
inputText->setMinimumSize(style::messages::smsInput::default_input_w,
style::messages::smsInput::default_input_h);
- inputText->setMaximumHeight(style::messages::smsInput::max_input_h);
inputText->setFont(style::window::font::medium);
inputText->setPadding(Padding(0, 0, 0, style::messages::smsInput::bottom_padding));
inputText->setPenFocusWidth(style::window::default_border_focus_w);
inputText->setPenWidth(style::window::default_border_focus_w);
inputText->setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
- replyImage = new Image(this, 0, 0, "messages_reply");
+ replyImage = new Image(body, 0, 0, "messages_reply");
replyImage->setAlignment(Alignment(gui::Alignment::Vertical::Bottom));
replyImage->setMargins(Margins(0, 0, 0, style::messages::smsInput::reply_bottom_margin));
replyImage->activeItem = false;
- inputText->inputCallback = [=](Item &, const InputEvent &event) {
+ inputCallback = [&]([[maybe_unused]] Item &item, const InputEvent &event) { return inputText->onInput(event); };
+
+ focusChangedCallback = [this]([[maybe_unused]] Item &item) {
+ setFocusItem(focus ? body : nullptr);
+ return true;
+ };
+
+ inputText->inputCallback = [this, application]([[maybe_unused]] Item &, const InputEvent &event) {
if (event.state == InputEvent::State::keyReleasedShort && event.keyCode == KeyCode::KEY_LF) {
auto app = dynamic_cast<app::ApplicationMessages *>(application);
assert(app != nullptr);
@@ 45,11 57,14 @@ namespace gui
return false;
};
- inputText->focusChangedCallback = [=](Item &) -> bool {
+ inputText->focusChangedCallback = [this, application]([[maybe_unused]] Item &) -> bool {
+ assert(body != nullptr);
+ assert(application != nullptr);
+
if (inputText->focus) {
- application->getCurrentWindow()->setBottomBarText(utils::localize.get("sms_reply"),
- BottomBar::Side::CENTER);
+ application->getWindow(gui::name::window::thread_view)
+ ->setBottomBarText(utils::localize.get("sms_reply"), BottomBar::Side::CENTER);
inputText->setInputMode(new InputMode(
{InputMode::ABC, InputMode::abc, InputMode::digit},
@@ 57,7 72,7 @@ namespace gui
[=]() { application->getCurrentWindow()->bottomBarRestoreFromTemporaryMode(); },
[=]() { application->getCurrentWindow()->selectSpecialCharacter(); }));
- if (inputText->getText().getLine() == utils::localize.get("sms_temp_reply")) {
+ if (inputText->getText() == utils::localize.get("sms_temp_reply")) {
inputText->clear();
}
}
@@ 72,11 87,86 @@ namespace gui
}
}
- application->getCurrentWindow()->clearBottomBarText(BottomBar::Side::CENTER);
+ application->getWindow(gui::name::window::thread_view)->clearBottomBarText(BottomBar::Side::CENTER);
}
return true;
};
}
+ void SMSInputWidget::handleDraftMessage()
+ {
+ if (const auto &text = inputText->getText(); text.empty() || (text == utils::localize.get("sms_temp_reply"))) {
+ clearDraftMessage();
+ }
+ else {
+ updateDraftMessage(text);
+ }
+ }
+
+ void SMSInputWidget::clearDraftMessage()
+ {
+ if (!draft.has_value()) {
+ displayDraftMessage();
+ return;
+ }
+
+ auto app = dynamic_cast<app::ApplicationMessages *>(application);
+ assert(app != nullptr);
+ if (const auto removed = app->removeDraft(draft.value()); removed) {
+ draft = std::nullopt;
+ displayDraftMessage();
+ }
+ }
+
+ void SMSInputWidget::displayDraftMessage() const
+ {
+ if (draft.has_value()) {
+ inputText->setText(draft->body);
+ }
+ else {
+ inputText->clear();
+ }
+ }
+
+ void SMSInputWidget::updateDraftMessage(const UTF8 &inputText)
+ {
+ auto app = dynamic_cast<app::ApplicationMessages *>(application);
+ assert(app != nullptr);
+ assert(number != nullptr);
+
+ if (draft.has_value()) {
+ app->updateDraft(draft.value(), inputText);
+ }
+ else {
+ const auto &[draft, success] = app->createDraft(*number, inputText);
+ if (success) {
+ this->draft = draft;
+ }
+ }
+ }
+
+ auto SMSInputWidget::onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool
+ {
+ body->setPosition(0, 0);
+ body->setSize(newDim.w, newDim.h);
+
+ return true;
+ }
+
+ auto SMSInputWidget::handleRequestResize([[maybe_unused]] const Item *child,
+ unsigned short request_w,
+ unsigned short request_h) -> Size
+ {
+ request_h =
+ std::clamp((Length)request_h, style::messages::smsInput::min_h, style::messages::smsInput::max_input_h);
+
+ setMinimumHeight(request_h);
+ if (parent != nullptr) {
+ requestSize(request_w, request_h);
+ }
+
+ return Size(request_w, request_h);
+ }
+
} /* namespace gui */
M module-apps/application-messages/widgets/SMSInputWidget.hpp => module-apps/application-messages/widgets/SMSInputWidget.hpp +18 -4
@@ 4,20 4,34 @@
#include "Text.hpp"
#include "Image.hpp"
+#include "ListItem.hpp"
#include <BoxLayout.hpp>
+#include "Interface/SMSRecord.hpp"
namespace gui
{
- class SMSInputWidget : public HBox
+ class SMSInputWidget : public ListItem
{
- gui::Image *replyImage = nullptr;
+ app::Application *application = nullptr;
+ HBox *body = nullptr;
+ gui::Image *replyImage = nullptr;
public:
gui::Text *inputText = nullptr;
+ std::optional<SMSRecord> draft; // draft message of the thread we are showing, if exists.
+ std::unique_ptr<utils::PhoneNumber::View> number = nullptr;
- SMSInputWidget(Item *parent, app::Application *application);
- virtual ~SMSInputWidget() = default;
+ SMSInputWidget(app::Application *application);
+ ~SMSInputWidget() override = default;
+
+ void handleDraftMessage();
+ void clearDraftMessage();
+ void updateDraftMessage(const UTF8 &inputText);
+ void displayDraftMessage() const;
+
+ auto onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool override;
+ auto handleRequestResize(const Item *, unsigned short request_w, unsigned short request_h) -> Size override;
};
} /* namespace gui */
A module-apps/application-messages/widgets/SMSOutputWidget.cpp => module-apps/application-messages/widgets/SMSOutputWidget.cpp +157 -0
@@ 0,0 1,157 @@
+#include <application-messages/ApplicationMessages.hpp>
+#include "application-messages/windows/OptionsMessages.hpp"
+#include <OptionWindow.hpp>
+#include "SMSOutputWidget.hpp"
+#include "application-messages/data/MessagesStyle.hpp"
+
+#include <Style.hpp>
+#include <time/time_conversion.hpp>
+
+namespace gui
+{
+
+ SMSOutputWidget::SMSOutputWidget(app::Application *application, const std::shared_ptr<SMSRecord> &record)
+ {
+ setMinimumSize(style::window::default_body_width, style::messages::smsOutput::default_h);
+ setMargins(Margins(0, style::messages::smsOutput::sms_vertical_spacer, 0, 0));
+ setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
+
+ body = new HBox(this, 0, 0, 0, 0);
+ body->setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
+ body->setMaximumSize(style::window::default_body_width, style::window::default_body_height);
+
+ smsBubble = new TextBubble(nullptr, 0, 0, 0, 0);
+ smsBubble->setMaximumSize(style::messages::smsOutput::sms_max_width, style::window::default_body_height);
+ smsBubble->setAlignment(Alignment(Alignment::Vertical::Center));
+ smsBubble->setTextType(TextType::MULTI_LINE);
+ smsBubble->setRadius(style::messages::smsOutput::sms_radius);
+ smsBubble->setFont(style::window::font::medium);
+ smsBubble->setPenFocusWidth(style::window::default_border_focus_w);
+ smsBubble->setPenWidth(style::window::default_border_rect_no_focus);
+ smsBubble->setPadding(style::messages::smsOutput::sms_right_bubble_padding);
+
+ LOG_DEBUG("ADD SMS TYPE: %d", static_cast<int>(record->type));
+ switch (record->type) {
+ case SMSType::QUEUED:
+ // Handle in the same way as case below. (pending sending display as already sent)
+ [[fallthrough]];
+ case SMSType::OUTBOX:
+ smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_RIGHT);
+ body->setReverseOrder(true);
+ body->addWidget(smsBubble);
+ timeLabelBuild(record->date);
+ break;
+ case SMSType::INBOX:
+ smsBubble->setPadding(style::messages::smsOutput::sms_left_bubble_padding);
+ smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_LEFT);
+ body->setReverseOrder(false);
+ body->addWidget(smsBubble);
+ timeLabelBuild(record->date);
+ break;
+ case SMSType::FAILED:
+ smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_RIGHT);
+ body->setReverseOrder(true);
+ errorIconBuild();
+ body->addWidget(smsBubble);
+ break;
+ case SMSType::DRAFT:
+ LOG_ERROR("Can't handle Draft type message in smsBubble");
+ break;
+ default:
+ break;
+ }
+
+ smsBubble->setText(record->body);
+
+ focusChangedCallback = [this]([[maybe_unused]] Item &item) {
+ setFocusItem(focus ? body : nullptr);
+ return false;
+ };
+
+ body->focusChangedCallback = [this]([[maybe_unused]] Item &item) {
+ if (timeLabel != nullptr) {
+ timeLabel->setVisible(focus);
+ body->resizeItems();
+ }
+ return true;
+ };
+
+ inputCallback = [&]([[maybe_unused]] Item &item, const InputEvent &event) { return smsBubble->onInput(event); };
+
+ smsBubble->inputCallback = [application, record](Item &, const InputEvent &event) {
+ if (event.state == InputEvent::State::keyReleasedShort && event.keyCode == KeyCode::KEY_LF) {
+ LOG_INFO("Message activated!");
+ auto app = dynamic_cast<app::ApplicationMessages *>(application);
+ assert(app != nullptr);
+ if (app->windowOptions != nullptr) {
+ app->windowOptions->clearOptions();
+ app->windowOptions->addOptions(smsWindowOptions(app, *record));
+ app->switchWindow(app->windowOptions->getName(), nullptr);
+ }
+ return true;
+ }
+ return false;
+ };
+ }
+
+ void SMSOutputWidget::positionTimeLabel() const
+ {
+ if (timeLabel != nullptr) {
+ timeLabel->setMinimumWidth(timeLabel->getTextNeedSpace());
+ timeLabel->setMinimumHeight(style::messages::smsOutput::default_h);
+ uint16_t timeLabelMargin = body->getWidth() - (smsBubble->getWidth() + timeLabel->getTextNeedSpace());
+
+ if (body->getReverseOrder()) {
+ timeLabel->setMargins(Margins(0, 0, timeLabelMargin, 0));
+ }
+ else {
+ timeLabel->setMargins(Margins(timeLabelMargin, 0, 0, 0));
+ }
+
+ body->resizeItems();
+ }
+ }
+
+ void SMSOutputWidget::timeLabelBuild(time_t timestamp)
+ {
+ timeLabel = new gui::Label(body, 0, 0, 0, 0);
+ timeLabel->activeItem = false;
+ timeLabel->setFont(style::window::font::verysmall);
+ timeLabel->setText(utils::time::Time(timestamp));
+ timeLabel->setVisible(false);
+ timeLabel->setAlignment(gui::Alignment(gui::Alignment::Horizontal::Center, gui::Alignment::Vertical::Center));
+ timeLabel->setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
+ }
+
+ void SMSOutputWidget::errorIconBuild()
+ {
+ errorIcon = new gui::Image("messages_error_W_M");
+ errorIcon->setAlignment(Alignment(Alignment::Vertical::Center));
+ errorIcon->activeItem = false;
+ errorIcon->setMargins(Margins(style::messages::smsOutput::sms_error_icon_left_margin,
+ 0,
+ style::messages::smsOutput::sms_error_icon_right_margin,
+ 0));
+ body->addWidget(errorIcon);
+ }
+
+ auto SMSOutputWidget::onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool
+ {
+ body->setPosition(0, 0);
+ body->setSize(newDim.w, newDim.h);
+
+ // We need to calculate margin between sms and timeLabel and we can do it only after sizes are set.
+ positionTimeLabel();
+
+ return true;
+ }
+
+ auto SMSOutputWidget::handleRequestResize([[maybe_unused]] const Item *child,
+ unsigned short request_w,
+ unsigned short request_h) -> Size
+ {
+ setMinimumHeight(request_h);
+ return Size(request_w, request_h);
+ }
+
+} /* namespace gui */
A module-apps/application-messages/widgets/SMSOutputWidget.hpp => module-apps/application-messages/widgets/SMSOutputWidget.hpp +35 -0
@@ 0,0 1,35 @@
+#pragma once
+
+#include "Application.hpp"
+
+#include "Text.hpp"
+#include "TextBubble.hpp"
+#include "Image.hpp"
+#include "ListItem.hpp"
+#include <BoxLayout.hpp>
+
+namespace gui
+{
+
+ class SMSOutputWidget : public ListItem
+ {
+ HBox *body = nullptr;
+ TextBubble *smsBubble = nullptr;
+ Label *timeLabel = nullptr;
+ Image *errorIcon = nullptr;
+
+ void timeLabelBuild(time_t timestamp);
+ void errorIconBuild();
+ void positionTimeLabel() const;
+
+ public:
+ gui::Text *inputText = nullptr;
+
+ SMSOutputWidget(app::Application *application, const std::shared_ptr<SMSRecord> &record);
+ virtual ~SMSOutputWidget() = default;
+
+ auto onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool override;
+ auto handleRequestResize(const Item *, unsigned short request_w, unsigned short request_h) -> Size override;
+ };
+
+} /* namespace gui */
M module-apps/application-messages/windows/NewMessage.cpp => module-apps/application-messages/windows/NewMessage.cpp +2 -2
@@ 177,7 177,7 @@ namespace gui
reciepientHbox->setAlignment(gui::Alignment::Vertical::Center);
reciepientHbox->setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_BOTTOM);
reciepientHbox->setPenFocusWidth(style::window::default_border_focus_w);
- reciepientHbox->setPenWidth(style::window::messages::sms_border_no_focus);
+ reciepientHbox->setPenWidth(style::window::default_border_rect_no_focus);
recipient = new gui::Text(reciepientHbox,
0,
@@ 221,7 221,7 @@ namespace gui
[=]() { bottomBarRestoreFromTemporaryMode(); },
[=]() { selectSpecialCharacter(); }));
message->setPenFocusWidth(style::window::default_border_focus_w);
- message->setPenWidth(style::window::messages::sms_border_no_focus);
+ message->setPenWidth(style::window::default_border_rect_no_focus);
message->setFont(style::window::font::medium);
message->setAlignment(Alignment(gui::Alignment::Horizontal::Left, gui::Alignment::Vertical::Center));
message->activatedCallback = [=](Item &) -> bool {
M module-apps/application-messages/windows/SMSThreadViewWindow.cpp => module-apps/application-messages/windows/SMSThreadViewWindow.cpp +34 -405
@@ 2,411 2,46 @@
#include "application-messages/ApplicationMessages.hpp"
#include "application-messages/data/SMSdata.hpp"
+#include "application-messages/data/MessagesStyle.hpp"
+
#include "OptionsMessages.hpp"
+#include <OptionWindow.hpp>
#include "Service/Message.hpp"
-#include <Text.hpp>
#include <TextBubble.hpp>
-#include <Label.hpp>
-#include <Margins.hpp>
#include <service-db/api/DBServiceAPI.hpp>
-#include <service-appmgr/ApplicationManager.hpp>
#include <service-db/messages/DBNotificationMessage.hpp>
-#include <i18/i18.hpp>
-#include <time/time_conversion.hpp>
#include <log/log.hpp>
#include <Style.hpp>
-#include <OptionWindow.hpp>
-#include <memory>
#include <cassert>
namespace gui
{
- SMSThreadViewWindow::SMSThreadViewWindow(app::Application *app) : AppWindow(app, name::window::thread_view)
+ SMSThreadViewWindow::SMSThreadViewWindow(app::Application *app)
+ : AppWindow(app, name::window::thread_view), smsModel{std::make_shared<SMSThreadModel>(this->application)}
{
AppWindow::buildInterface();
setTitle(utils::localize.get("app_messages_title_main"));
topBar->setActive(TopBar::Elements::TIME, true);
bottomBar->setText(BottomBar::Side::LEFT, utils::localize.get(style::strings::common::options));
bottomBar->setText(BottomBar::Side::RIGHT, utils::localize.get(style::strings::common::back));
- body = new gui::VBox(this,
- style::window::default_left_margin,
- title->offset_h(),
- elements_width,
- bottomBar->getY() - title->offset_h());
- body->setPenWidth(style::window::default_border_no_focus_w);
- body->setPenFocusWidth(style::window::default_border_no_focus_w);
- body->borderCallback = [this](const InputEvent &inputEvent) -> bool {
- if (inputEvent.state != InputEvent::State::keyReleasedShort) {
- return false;
- }
- if (inputEvent.keyCode == KeyCode::KEY_UP) {
- return this->showMessages(Action::NextPage);
- }
- else if (inputEvent.keyCode == KeyCode::KEY_DOWN) {
- return this->showMessages(Action::PreviousPage);
- }
- else {
- return false;
- }
- };
-
- refreshTextItem();
- /// setup
- body->setReverseOrder(true);
- body->setVisible(true);
- setFocusItem(body);
- }
-
- void SMSThreadViewWindow::handleDraftMessage()
- {
- if (const auto &text = inputMessage->inputText->getText();
- text.empty() || (text == utils::localize.get("sms_temp_reply"))) {
- clearDraftMessage();
- }
- else {
- updateDraftMessage(text);
- }
- }
-
- void SMSThreadViewWindow::clearDraftMessage()
- {
- if (!SMS.draft.has_value()) {
- displayDraftMessage();
- return;
- }
-
- auto app = dynamic_cast<app::ApplicationMessages *>(application);
- assert(app != nullptr);
- if (const auto removed = app->removeDraft(SMS.draft.value()); removed) {
- SMS.draft = std::nullopt;
- displayDraftMessage();
- }
- }
-
- void SMSThreadViewWindow::displayDraftMessage() const
- {
- if (SMS.draft.has_value()) {
- inputMessage->inputText->setText(SMS.draft->body);
- }
- else {
- inputMessage->inputText->clear();
- }
- }
-
- void SMSThreadViewWindow::updateDraftMessage(const UTF8 &inputText)
- {
- auto app = dynamic_cast<app::ApplicationMessages *>(application);
- assert(app != nullptr);
-
- if (SMS.draft.has_value()) {
- app->updateDraft(SMS.draft.value(), inputText);
- }
- else {
- const auto &[draft, success] = app->createDraft(*number, inputText);
- if (success) {
- SMS.draft = draft;
- }
- }
- }
- void SMSThreadViewWindow::refreshTextItem()
- {
- if (inputMessage != nullptr) {
- return;
- }
+ smsList = new gui::ListView(this,
+ style::messages::smsList::x,
+ style::messages::smsList::y,
+ style::messages::smsList::w,
+ style::messages::smsList::h,
+ smsModel);
+ smsList->setOrientation(style::listview::Orientation::BottomTop);
- inputMessage = new SMSInputWidget(body, application);
- inputMessage->activatedCallback = [this]([[maybe_unused]] gui::Item &item) {
- auto app = dynamic_cast<app::ApplicationMessages *>(application);
- assert(app != nullptr);
- if (app->handleSendSmsFromThread(*number, inputMessage->inputText->getText())) {
- LOG_ERROR("handleSendSmsFromThread failed");
- }
- clearDraftMessage();
- return true;
- };
- }
-
- void SMSThreadViewWindow::destroyTextItem()
- {
- body->erase(inputMessage);
- if (inputMessage->parent == nullptr) {
- delete (inputMessage);
- }
- inputMessage = nullptr;
- }
-
- void SMSThreadViewWindow::cleanView()
- {
- body->removeWidget(inputMessage);
- body->erase();
- }
-
- bool SMSThreadViewWindow::showMessages(SMSThreadViewWindow::Action what)
- {
- if (SMS.thread <= 0) {
- LOG_ERROR("threadID not set!");
- return false;
- }
- addSMS(what);
- return true;
- }
-
- void SMSThreadViewWindow::addSMS(SMSThreadViewWindow::Action what)
- {
- LOG_DEBUG("--- %d ---", static_cast<int>(what));
- // if there was text - then remove it temp
- // 1. load elements to tmp vector
- std::unique_ptr<ThreadRecord> threadDetails = DBServiceAPI::ThreadGet(this->application, SMS.thread);
- if (threadDetails == nullptr) {
- LOG_ERROR("cannot fetch details of selected thread (id: %d)", SMS.thread);
- return;
- }
- SMS.dbsize = threadDetails->msgCount;
-
- if (threadDetails->isUnread()) {
- auto app = dynamic_cast<app::ApplicationMessages *>(application);
- assert(app != nullptr);
- if (application->getCurrentWindow() == this) {
- app->markSmsThreadAsRead(threadDetails->ID);
- }
- }
-
- LOG_DEBUG("start: %d end: %d db: %d", SMS.start, SMS.end, SMS.dbsize);
- if (what == Action::Init || what == Action::NewestPage) {
- SMS.start = 0;
-
- // Refactor
- SMS.end = maxsmsinwindow;
- if (what == Action::Init) {
- destroyTextItem();
- }
- refreshTextItem();
- }
-
- // 2. check how many of these will fit in box
- // update begin / end in `SMS`
- if (what == Action::NextPage) {
- if (SMS.end != SMS.dbsize) {
-
- // Refactor
- for (auto sms : body->children) {
- if (sms->visible)
- SMS.start++;
- }
-
- if (inputMessage->visible)
- SMS.start -= 1;
-
- LOG_INFO("SMS start %d", SMS.start);
- }
- else {
- LOG_INFO("All sms shown");
- return;
- }
- }
- else if (what == Action::PreviousPage) {
- if (SMS.start == 0) {
- return;
- }
- else if (SMS.start - maxsmsinwindow < 0) {
- SMS.start = 0;
- }
- else {
- SMS.start -= maxsmsinwindow;
- }
- LOG_DEBUG("in progress %d", SMS.start);
- }
- SMS.sms = DBServiceAPI::SMSGetLimitOffsetByThreadID(this->application, SMS.start, maxsmsinwindow, SMS.thread);
- LOG_DEBUG("=> SMS %d < %d < %d",
- static_cast<int>(SMS.start),
- static_cast<int>(SMS.sms->size()),
- static_cast<int>(maxsmsinwindow));
- if (SMS.sms->size() == 0) {
- LOG_WARN("Malformed thread. Leave it (id: %d)", SMS.thread);
- application->switchWindow(gui::name::window::main_window);
- return;
- }
-
- if (what == Action::Init) {
- const auto &lastSms = SMS.sms->front();
- SMS.draft = lastSms.type == SMSType::DRAFT ? std::optional<SMSRecord>{lastSms} : std::nullopt;
- displayDraftMessage();
- }
-
- // 3. add them to box
- this->cleanView();
- // if we are going from 0 then we want to show text prompt
- if (SMS.start == 0) {
- body->addWidget(inputMessage);
- }
-
- // rebuild bubbles
- SMS.end = SMS.start;
- for (auto &el : *SMS.sms) {
- if (el.type != SMSType::DRAFT) {
- if (!smsBuild(el)) {
- break;
- }
- ++SMS.end;
- }
- }
-
- body->setNavigation();
- setFocusItem(body);
- if (Action::PreviousPage == what) {
- body->setVisible(true, true);
- }
- LOG_DEBUG("sms built");
- }
-
- HBox *SMSThreadViewWindow::smsSpanBuild(Text *smsBubble, const SMSRecord &el) const
- {
- HBox *labelSpan = new gui::HBox();
-
- labelSpan->setPenWidth(style::window::default_border_no_focus_w);
- labelSpan->setPenFocusWidth(style::window::default_border_no_focus_w);
- labelSpan->setSize(elements_width, smsBubble->getHeight());
- labelSpan->setMinimumWidth(elements_width);
- labelSpan->setMinimumHeight(smsBubble->getHeight());
- labelSpan->setMaximumHeight(smsBubble->widgetMaximumArea.h);
- labelSpan->setFillColor(gui::Color(11, 0));
-
- LOG_DEBUG("ADD SMS TYPE: %d", static_cast<int>(el.type));
- switch (el.type) {
- case SMSType::QUEUED:
- // Handle in the same way as case below. (pending sending display as already sent)
- [[fallthrough]];
- case SMSType::OUTBOX:
- smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_RIGHT);
- smsBubble->setX(body->getWidth() - smsBubble->getWidth());
- labelSpan->setReverseOrder(true);
- labelSpan->addWidget(smsBubble);
- addTimeLabel(
- labelSpan, timeLabelBuild(el.date), elements_width - (smsBubble->getWidth() + smsBubble->yapSize));
- break;
- case SMSType::INBOX:
- smsBubble->setPadding(style::window::messages::sms_left_bubble_padding);
- smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_LEFT);
- labelSpan->setReverseOrder(false);
- labelSpan->addWidget(smsBubble);
- addTimeLabel(
- labelSpan, timeLabelBuild(el.date), elements_width - (smsBubble->getWidth() + smsBubble->yapSize));
- break;
- case SMSType::FAILED:
- smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_RIGHT);
- smsBubble->setX(body->getWidth() - smsBubble->getWidth());
- labelSpan->setReverseOrder(true);
- addErrorIcon(labelSpan);
- labelSpan->addWidget(smsBubble);
- break;
- default:
- break;
- }
-
- if (!smsBubble->visible) {
- delete labelSpan; // total fail
- labelSpan = nullptr;
- }
- return labelSpan;
- }
-
- void SMSThreadViewWindow::addErrorIcon(HBox *layout) const
- {
- auto errorIcon = new gui::Image("messages_error_W_M");
- errorIcon->setAlignment(Alignment(Alignment::Vertical::Center));
- errorIcon->activeItem = false; // make it non-focusable
- errorIcon->setMargins(Margins(style::window::messages::sms_error_icon_offset,
- 0,
- (style::window::messages::sms_failed_offset -
- (errorIcon->getWidth() + style::window::messages::sms_error_icon_offset)),
- 0));
- layout->addWidget(errorIcon);
- }
-
- void SMSThreadViewWindow::addTimeLabel(HBox *layout, Label *timeLabel, uint16_t widthAvailable) const
- {
- // add time label activated on focus
- timeLabel->setMinimumWidth(timeLabel->getTextNeedSpace());
- timeLabel->setMinimumHeight(layout->getHeight());
- timeLabel->setSize(timeLabel->getTextNeedSpace(), layout->getHeight());
-
- uint16_t timeLabelSpacerWidth = widthAvailable - timeLabel->getWidth();
-
- timeLabel->setMargins(Margins(timeLabelSpacerWidth, 0, timeLabelSpacerWidth, 0));
- layout->addWidget(timeLabel);
-
- layout->focusChangedCallback = [=](gui::Item &item) {
- timeLabel->setVisible(item.focus);
- // we need to inform parent that it needs to resize itself - easiest way to do so
- if (timeLabel->parent) {
- timeLabel->parent->setSize(timeLabel->parent->getWidth(), timeLabel->parent->getHeight());
- }
- return true;
- };
- }
-
- Label *SMSThreadViewWindow::timeLabelBuild(time_t timestamp) const
- {
- auto timeLabel = new gui::Label(nullptr, 0, 0, 0, 0);
- timeLabel->activeItem = false;
- timeLabel->setFont(style::window::font::verysmall);
- timeLabel->setText(utils::time::Time(timestamp));
- timeLabel->setPenWidth(style::window::default_border_no_focus_w);
- timeLabel->setVisible(false);
- timeLabel->setAlignment(gui::Alignment(gui::Alignment::Horizontal::Center, gui::Alignment::Vertical::Center));
- return timeLabel;
- }
-
- bool SMSThreadViewWindow::smsBuild(const SMSRecord &smsRecord)
- {
- auto max_available_h = body->area().h;
- auto max_available_w = style::window::messages::sms_max_width;
- /// dummy sms thread - TODO load from db - on switchData
- auto smsBubble = new TextBubble(nullptr, 0, 0, style::window::messages::sms_max_width, 0);
- smsBubble->setMaximumSize(max_available_w, max_available_h);
- smsBubble->setTextType(TextType::MULTI_LINE);
- smsBubble->setRadius(style::window::messages::sms_radius);
- smsBubble->setFont(style::window::font::medium);
- smsBubble->setPenFocusWidth(style::window::default_border_focus_w);
- smsBubble->setPenWidth(style::window::messages::sms_border_no_focus);
- smsBubble->setPadding(style::window::messages::sms_right_bubble_padding);
- smsBubble->setText(smsRecord.body);
-
- smsBubble->inputCallback = [=, &smsRecord](Item &, const InputEvent &event) {
- if (event.state == InputEvent::State::keyReleasedShort && event.keyCode == KeyCode::KEY_LF) {
- LOG_INFO("Message activated!");
- auto app = dynamic_cast<app::ApplicationMessages *>(application);
- assert(app != nullptr);
- if (app->windowOptions != nullptr) {
- app->windowOptions->clearOptions();
- app->windowOptions->addOptions(smsWindowOptions(app, smsRecord));
- app->switchWindow(app->windowOptions->getName(), nullptr);
- }
- return true;
- }
- return false;
- };
-
- // wrap label in H box, to make fit datetime in it
- HBox *labelSpan = smsSpanBuild(smsBubble, smsRecord);
- labelSpan->setMargins(Margins(0, style::window::messages::sms_vertical_spacer, 0, 0));
-
- if (labelSpan == nullptr) {
- return false;
- }
-
- LOG_INFO("Add sms: %s %s", smsRecord.body.c_str(), smsRecord.number.getFormatted().c_str());
- body->addWidget(labelSpan);
- return labelSpan->visible;
+ setFocusItem(smsList);
}
void SMSThreadViewWindow::rebuild()
{
- addSMS(SMSThreadViewWindow::Action::Init);
+ smsList->rebuildList();
}
void SMSThreadViewWindow::buildInterface()
@@ 428,25 63,33 @@ namespace gui
auto pdata = dynamic_cast<SMSThreadData *>(data);
if (pdata) {
LOG_INFO("We have it! %" PRIu32, pdata->thread->ID);
- cleanView();
- SMS.thread = pdata->thread->ID;
- showMessages(Action::Init);
auto ret = DBServiceAPI::ContactGetByIDWithTemporary(application, pdata->thread->contactID);
contact = std::make_shared<ContactRecord>(ret->front());
// should be name number for now - easier to handle
setTitle(ret->front().getFormattedName());
auto retNumber = DBServiceAPI::GetNumberById(application, pdata->thread->numberID, numberIdTimeout);
assert(retNumber != nullptr);
- number = std::move(retNumber);
- LOG_INFO("Phone number for thread: %s", number->getFormatted().c_str());
+ smsModel->number = std::move(retNumber);
+ LOG_INFO("Phonenumber for thread: %s", smsModel->number->getFormatted().c_str());
+
+ // Mark thread as Read
+ if (pdata->thread->isUnread()) {
+ auto app = dynamic_cast<app::ApplicationMessages *>(application);
+ assert(app != nullptr);
+ if (application->getCurrentWindow() == this) {
+ app->markSmsThreadAsRead(pdata->thread->ID);
+ }
+ }
+
+ smsModel->smsThreadID = pdata->thread->ID;
+ smsList->rebuildList();
}
}
if (auto pdata = dynamic_cast<SMSTextData *>(data)) {
auto txt = pdata->text;
LOG_INFO("received sms templates data \"%s\"", txt.c_str());
- pdata->concatenate == SMSTextData::Concatenate::True ? inputMessage->inputText->addText(txt)
- : inputMessage->inputText->setText(txt);
- body->resizeItems();
+ pdata->concatenate == SMSTextData::Concatenate::True ? smsModel->smsInput->inputText->addText(txt)
+ : smsModel->smsInput->inputText->setText(txt);
}
}
@@ 457,7 100,7 @@ namespace gui
void SMSThreadViewWindow::onClose()
{
- handleDraftMessage();
+ smsModel->handleDraftMessage();
}
bool SMSThreadViewWindow::onDatabaseMessage(sys::Message *msgl)
@@ 465,24 108,10 @@ namespace gui
auto msg = dynamic_cast<db::NotificationMessage *>(msgl);
if (msg != nullptr) {
if (msg->interface == db::Interface::Name::SMS) {
- std::unique_ptr<ThreadRecord> threadDetails;
- switch (msg->type) {
- case db::Query::Type::Create:
- // jump to the latest SMS
- addSMS(SMSThreadViewWindow::Action::NewestPage);
- break;
- case db::Query::Type::Update:
- case db::Query::Type::Delete:
- addSMS(SMSThreadViewWindow::Action::Refresh);
- break;
- case db::Query::Type::Read:
- // do not update view, as we don't have visual representation for read status
- break;
- }
- if (this == application->getCurrentWindow()) {
- application->refreshWindow(gui::RefreshModes::GUI_REFRESH_FAST);
+ if (msg->dataModified()) {
+ rebuild();
+ return true;
}
- return true;
}
if (msg->interface == db::Interface::Name::SMSThread) {
if (msg->type == db::Query::Type::Delete) {
M module-apps/application-messages/windows/SMSThreadViewWindow.hpp => module-apps/application-messages/windows/SMSThreadViewWindow.hpp +4 -45
@@ 1,15 1,11 @@
#pragma once
#include <AppWindow.hpp>
-#include <application-messages/widgets/SMSInputWidget.hpp>
-#include <gui/widgets/BoxLayout.hpp>
-#include <gui/widgets/Image.hpp>
-#include <gui/widgets/Label.hpp>
-#include <gui/widgets/Window.hpp>
+#include <module-apps/application-messages/models/SMSThreadModel.hpp>
+
#include <ListView.hpp>
#include <PhoneNumber.hpp>
#include <service-db/api/DBServiceAPI.hpp>
-#include <Text.hpp>
#include <functional>
#include <string>
@@ 19,39 15,11 @@ namespace gui
class SMSThreadViewWindow : public AppWindow
{
private:
- gui::VBox *body = nullptr;
- uint16_t elements_width = this->getWidth() - style::window::default_left_margin * 2;
- void cleanView();
- enum class Action
- {
- Init, /// first load of sms thread view
- NewestPage, /// show a sms thread page from the latest sms
- Refresh, /// just refresh current view
- NextPage, /// load previous page
- PreviousPage /// load next page
- };
- /// return if request was handled
- bool showMessages(Action what);
- void addSMS(Action what);
- bool smsBuild(const SMSRecord &smsRecord);
- Label *timeLabelBuild(time_t timestamp) const;
- HBox *smsSpanBuild(Text *smsBubble, const SMSRecord &el) const;
- const ssize_t maxsmsinwindow = 7;
+ std::shared_ptr<SMSThreadModel> smsModel = nullptr;
+ gui::ListView *smsList = nullptr;
std::shared_ptr<ContactRecord> contact;
- std::unique_ptr<utils::PhoneNumber::View> number;
-
- struct
- {
- int start = 0; // actual shown position start
- int end = 7; // actual shown position end
- int thread = 0; // thread we are showing
- int dbsize = 0; // size of elements in db
- std::unique_ptr<std::vector<SMSRecord>> sms; // loaded sms from db
- std::optional<SMSRecord> draft; // draft message of the thread we are showing, if exists.
- } SMS;
- gui::SMSInputWidget *inputMessage = nullptr;
inline static const std::uint32_t numberIdTimeout = 1000;
public:
@@ 69,15 37,6 @@ namespace gui
void buildInterface() override;
void destroyInterface() override;
- void destroyTextItem();
- void refreshTextItem();
- void addTimeLabel(HBox *layout, Label *timeLabel, uint16_t widthAvailable) const;
- void addErrorIcon(HBox *layout) const;
-
- void handleDraftMessage();
- void clearDraftMessage();
- void updateDraftMessage(const UTF8 &inputText);
- void displayDraftMessage() const;
};
} /* namespace gui */
M module-apps/application-phonebook/widgets/InputBoxWithLabelAndIconWidget.cpp => module-apps/application-phonebook/widgets/InputBoxWithLabelAndIconWidget.cpp +1 -1
@@ 24,7 24,7 @@ namespace gui
hBox = new gui::HBox(this, 0, 0, phonebookStyle::inputBoxWithLabelAndIconIWidget::w, 0);
hBox->setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
hBox->setPenFocusWidth(style::window::default_border_focus_w);
- hBox->setPenWidth(style::window::messages::sms_border_no_focus);
+ hBox->setPenWidth(style::window::default_border_rect_no_focus);
inputBoxLabel = new gui::Label(hBox, 0, 0, 0, 0);
inputBoxLabel->setMinimumSize(phonebookStyle::inputBoxWithLabelAndIconIWidget::input_box_w,
M module-apps/application-phonebook/widgets/PhonebookItem.cpp => module-apps/application-phonebook/widgets/PhonebookItem.cpp +1 -1
@@ 16,7 16,7 @@ namespace gui
hBox = new gui::HBox(this, 0, 0, 0, 0);
hBox->setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
hBox->setPenFocusWidth(style::window::default_border_focus_w);
- hBox->setPenWidth(style::window::messages::sms_border_no_focus);
+ hBox->setPenWidth(style::window::default_border_rect_no_focus);
contactName = new gui::Label(hBox, 0, 0, 0, 0);
contactName->setPenFocusWidth(0);
M module-apps/application-phonebook/windows/PhonebookIceContacts.cpp => module-apps/application-phonebook/windows/PhonebookIceContacts.cpp +5 -0
@@ 73,4 73,9 @@ namespace gui
return false;
}
+
+ void PhonebookIceContacts::onBeforeShow(ShowMode mode, SwitchData *data)
+ {
+ rebuild();
+ }
} /* namespace gui */
M module-apps/application-phonebook/windows/PhonebookIceContacts.hpp => module-apps/application-phonebook/windows/PhonebookIceContacts.hpp +1 -0
@@ 17,6 17,7 @@ namespace gui
bool onInput(const InputEvent &inputEvent) override;
bool onDatabaseMessage(sys::Message *msgl) override;
+ void onBeforeShow(ShowMode mode, SwitchData *data) override;
void rebuild() override;
void buildInterface() override;
M module-db/CMakeLists.txt => module-db/CMakeLists.txt +1 -0
@@ 67,6 67,7 @@ set(SOURCES
queries/messages/sms/QuerySMSGetByID.cpp
queries/messages/sms/QuerySMSGetByContactID.cpp
queries/messages/sms/QuerySMSGetByThreadID.cpp
+ queries/messages/sms/QuerySMSGetForList.cpp
queries/messages/sms/QuerySMSGetByText.cpp
queries/messages/sms/QuerySMSAdd.cpp
queries/messages/sms/QuerySMSRemove.cpp
M module-db/Common/Common.hpp => module-db/Common/Common.hpp +1 -0
@@ 20,6 20,7 @@ enum class SMSType : uint32_t
INBOX = 0x04,
OUTBOX = 0x08,
QUEUED = 0x10,
+ INPUT = 0x12,
UNKNOWN = 0xFF
};
M module-db/Interface/SMSRecord.cpp => module-db/Interface/SMSRecord.cpp +41 -0
@@ 18,6 18,7 @@
#include <PhoneNumber.hpp>
#include <optional>
+#include <module-db/queries/messages/sms/QuerySMSGetForList.hpp>
SMSRecord::SMSRecord(const SMSTableRow &w, const utils::PhoneNumber::View &num)
: date(w.date), dateSent(w.dateSent), errorCode(w.errorCode), body(w.body), type(w.type), threadID(w.threadID),
@@ 310,6 311,9 @@ std::unique_ptr<db::QueryResult> SMSRecordInterface::runQuery(std::shared_ptr<db
else if (typeid(*query) == typeid(db::query::SMSGetByThreadID)) {
return getByThreadIDQuery(query);
}
+ else if (typeid(*query) == typeid(db::query::SMSGetForList)) {
+ return getForListQuery(query);
+ }
else if (typeid(*query) == typeid(db::query::SMSGetCountByThreadID)) {
return getCountByThreadIDQuery(query);
}
@@ 509,6 513,43 @@ std::unique_ptr<db::QueryResult> SMSRecordInterface::getByThreadIDQuery(std::sha
return response;
}
+std::unique_ptr<db::QueryResult> SMSRecordInterface::getForListQuery(std::shared_ptr<db::Query> query)
+{
+ const auto localQuery = static_cast<const db::query::SMSGetForList *>(query.get());
+ auto smsVector =
+ smsDB->sms.getByThreadIdWithoutDraftWithEmptyInput(localQuery->threadId, localQuery->offset, localQuery->limit);
+ std::vector<SMSRecord> recordVector;
+ for (auto sms : smsVector) {
+ SMSRecord record;
+ record.body = sms.body;
+ record.contactID = sms.contactID;
+ record.date = sms.date;
+ record.dateSent = sms.dateSent;
+ record.errorCode = sms.errorCode;
+ record.threadID = sms.threadID;
+ record.type = sms.type;
+ record.ID = sms.ID;
+ recordVector.emplace_back(record);
+ }
+
+ auto count = smsDB->sms.countWithoutDraftsByThreadId(localQuery->threadId);
+ auto draft = smsDB->sms.getDraftByThreadId(localQuery->threadId);
+
+ SMSRecord record;
+ record.body = draft.body;
+ record.contactID = draft.contactID;
+ record.date = draft.date;
+ record.dateSent = draft.dateSent;
+ record.errorCode = draft.errorCode;
+ record.threadID = draft.threadID;
+ record.type = draft.type;
+ record.ID = draft.ID;
+
+ auto response = std::make_unique<db::query::SMSGetForListResult>(recordVector, count, record);
+ response->setRequestQuery(query);
+ return response;
+}
+
std::unique_ptr<db::QueryResult> SMSRecordInterface::getLastByThreadIDQuery(std::shared_ptr<db::Query> query)
{
const auto localQuery = static_cast<const db::query::SMSGetLastByThreadID *>(query.get());
M module-db/Interface/SMSRecord.hpp => module-db/Interface/SMSRecord.hpp +1 -0
@@ 79,6 79,7 @@ class SMSRecordInterface : public RecordInterface<SMSRecord, SMSRecordField>
std::unique_ptr<db::QueryResult> updateQuery(std::shared_ptr<db::Query> query);
std::unique_ptr<db::QueryResult> getQuery(std::shared_ptr<db::Query> query);
std::unique_ptr<db::QueryResult> getByThreadIDQuery(std::shared_ptr<db::Query> query);
+ std::unique_ptr<db::QueryResult> getForListQuery(std::shared_ptr<db::Query> query);
std::unique_ptr<db::QueryResult> getCountByThreadIDQuery(std::shared_ptr<db::Query> query);
std::unique_ptr<db::QueryResult> getLastByThreadIDQuery(std::shared_ptr<db::Query> query);
};
M module-db/Tables/SMSTable.cpp => module-db/Tables/SMSTable.cpp +67 -0
@@ 137,6 137,73 @@ std::vector<SMSTableRow> SMSTable::getByThreadId(uint32_t threadId, uint32_t off
return ret;
}
+std::vector<SMSTableRow> SMSTable::getByThreadIdWithoutDraftWithEmptyInput(uint32_t threadId,
+ uint32_t offset,
+ uint32_t limit)
+{
+ auto retQuery = db->query("SELECT * FROM sms WHERE thread_id= %u AND type != %u UNION ALL SELECT 0 as _id, 0 as "
+ "thread_id, 0 as contact_id, 0 as "
+ "date, 0 as date_send, 0 as error_code, 0 as body, %u as type LIMIT %u OFFSET %u",
+ threadId,
+ SMSType::DRAFT,
+ SMSType::INPUT,
+ limit,
+ offset);
+
+ if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
+ return std::vector<SMSTableRow>();
+ }
+
+ std::vector<SMSTableRow> ret;
+
+ do {
+ ret.push_back(SMSTableRow{
+ (*retQuery)[0].getUInt32(), // ID
+ (*retQuery)[1].getUInt32(), // threadID
+ (*retQuery)[2].getUInt32(), // contactID
+ (*retQuery)[3].getUInt32(), // date
+ (*retQuery)[4].getUInt32(), // dateSent
+ (*retQuery)[5].getUInt32(), // errorCode
+ (*retQuery)[6].getString(), // body
+ static_cast<SMSType>((*retQuery)[7].getUInt32()), // type
+ });
+ } while (retQuery->nextRow());
+
+ return ret;
+}
+
+uint32_t SMSTable::countWithoutDraftsByThreadId(uint32_t threadId)
+{
+ auto queryRet = db->query("SELECT COUNT(*) FROM sms WHERE thread_id= %u AND type != %u;", threadId, SMSType::DRAFT);
+
+ if (queryRet == nullptr || queryRet->getRowCount() == 0) {
+ return 0;
+ }
+
+ return uint32_t{(*queryRet)[0].getUInt32()};
+}
+
+SMSTableRow SMSTable::getDraftByThreadId(uint32_t threadId)
+{
+ auto retQuery = db->query(
+ "SELECT * FROM sms WHERE thread_id= %u AND type = %u ORDER BY date DESC LIMIT 1;", threadId, SMSType::DRAFT);
+
+ if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
+ return SMSTableRow();
+ }
+
+ return SMSTableRow{
+ (*retQuery)[0].getUInt32(), // ID
+ (*retQuery)[1].getUInt32(), // threadID
+ (*retQuery)[2].getUInt32(), // contactID
+ (*retQuery)[3].getUInt32(), // date
+ (*retQuery)[4].getUInt32(), // dateSent
+ (*retQuery)[5].getUInt32(), // errorCode
+ (*retQuery)[6].getString(), // body
+ static_cast<SMSType>((*retQuery)[7].getUInt32()), // type
+ };
+}
+
std::vector<SMSTableRow> SMSTable::getByText(std::string text)
{
M module-db/Tables/SMSTable.hpp => module-db/Tables/SMSTable.hpp +5 -0
@@ 47,6 47,11 @@ class SMSTable : public Table<SMSTableRow, SMSTableFields>
std::vector<SMSTableRow> getByContactId(uint32_t contactId);
std::vector<SMSTableRow> getByText(std::string text);
std::vector<SMSTableRow> getByThreadId(uint32_t threadId, uint32_t offset, uint32_t limit);
+ std::vector<SMSTableRow> getByThreadIdWithoutDraftWithEmptyInput(uint32_t threadId,
+ uint32_t offset,
+ uint32_t limit);
+ uint32_t countWithoutDraftsByThreadId(uint32_t threadId);
+ SMSTableRow getDraftByThreadId(uint32_t threadId);
std::pair<uint32_t, std::vector<SMSTableRow>> getManyByType(SMSType type, uint32_t offset, uint32_t limit);
A module-db/queries/messages/sms/QuerySMSGetForList.cpp => module-db/queries/messages/sms/QuerySMSGetForList.cpp +35 -0
@@ 0,0 1,35 @@
+#include "QuerySMSGetForList.hpp"
+
+#include <utility>
+
+namespace db::query
+{
+ SMSGetForList::SMSGetForList(unsigned int threadId, unsigned int offset, unsigned int limit)
+ : Query(Query::Type::Read), threadId(threadId), offset(offset), limit(limit)
+ {}
+
+ auto SMSGetForList::debugInfo() const -> std::string
+ {
+ return "SMSGetForList";
+ }
+
+ SMSGetForListResult::SMSGetForListResult(std::vector<SMSRecord> result, unsigned int count, SMSRecord draft)
+ : result(std::move(result)), count(count), draft(std::move(draft))
+ {}
+ auto SMSGetForListResult::getResults() const -> std::vector<SMSRecord>
+ {
+ return result;
+ }
+ auto SMSGetForListResult::getCount() const -> unsigned int
+ {
+ return count;
+ }
+ auto SMSGetForListResult::getDraft() const -> SMSRecord
+ {
+ return draft;
+ }
+ auto SMSGetForListResult::debugInfo() const -> std::string
+ {
+ return "SMSGetForListResult";
+ }
+} // namespace db::query
A module-db/queries/messages/sms/QuerySMSGetForList.hpp => module-db/queries/messages/sms/QuerySMSGetForList.hpp +36 -0
@@ 0,0 1,36 @@
+#pragma once
+
+#include <Tables/ThreadsTable.hpp>
+#include <Common/Query.hpp>
+#include <string>
+#include "Interface/SMSRecord.hpp"
+
+namespace db::query
+{
+
+ class SMSGetForList : public Query
+ {
+ public:
+ unsigned int threadId = DB_ID_NONE;
+ unsigned int offset = 0;
+ unsigned int limit = 0;
+
+ SMSGetForList(unsigned int id, unsigned int offset = 0, unsigned int limit = 0);
+ [[nodiscard]] auto debugInfo() const -> std::string override;
+ };
+
+ class SMSGetForListResult : public QueryResult
+ {
+ std::vector<SMSRecord> result;
+ unsigned int count = 0;
+ SMSRecord draft;
+
+ public:
+ SMSGetForListResult(std::vector<SMSRecord> result, unsigned int count, SMSRecord draft);
+ [[nodiscard]] auto getResults() const -> std::vector<SMSRecord>;
+ [[nodiscard]] auto getCount() const -> unsigned int;
+ [[nodiscard]] auto getDraft() const -> SMSRecord;
+ [[nodiscard]] auto debugInfo() const -> std::string override;
+ };
+
+} // namespace db::query
M module-db/tests/SMSRecord_tests.cpp => module-db/tests/SMSRecord_tests.cpp +195 -165
@@ 16,6 16,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
+#include <module-db/queries/messages/sms/QuerySMSGetForList.hpp>
struct test
{
@@ 52,183 53,212 @@ TEST_CASE("SMS Record tests")
recordIN.body = bodyTest;
recordIN.type = typeTest;
- // Add 2 records
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.Add(recordIN));
+ SECTION("SMS Record Test")
+ {
+ // Add 2 records
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
- // Get all available records
- auto records = smsRecInterface.GetLimitOffset(0, 100);
- REQUIRE(records->size() == 2);
+ // Get all available records
+ auto records = smsRecInterface.GetLimitOffset(0, 100);
+ REQUIRE(records->size() == 2);
- // Check if fetched records contain valid data
- for (const auto &w : *records) {
- REQUIRE(w.body == bodyTest);
- REQUIRE(w.number == numberTest);
- }
+ // Check if fetched records contain valid data
+ for (const auto &w : *records) {
+ REQUIRE(w.body == bodyTest);
+ REQUIRE(w.number == numberTest);
+ }
- // Get all available records by specified thread ID and check for invalid data
- records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1");
- REQUIRE((*records).size() == 2);
- for (const auto &w : *records) {
- REQUIRE(w.body == bodyTest);
- REQUIRE(w.number == numberTest);
- }
+ // Get all available records by specified thread ID and check for invalid data
+ records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1");
+ REQUIRE((*records).size() == 2);
+ for (const auto &w : *records) {
+ REQUIRE(w.body == bodyTest);
+ REQUIRE(w.number == numberTest);
+ }
- // Get all available records by specified contact ID and check for invalid data
- records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
- REQUIRE((*records).size() == 2);
- for (const auto &w : *records) {
- REQUIRE(w.body == bodyTest);
- REQUIRE(w.number == numberTest);
- }
+ // Get all available records by specified contact ID and check for invalid data
+ records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
+ REQUIRE((*records).size() == 2);
+ for (const auto &w : *records) {
+ REQUIRE(w.body == bodyTest);
+ REQUIRE(w.number == numberTest);
+ }
- // Remove records one by one
- REQUIRE(smsRecInterface.RemoveByID(1));
- REQUIRE(smsRecInterface.RemoveByID(2));
-
- // SMS database should not contain any records
- REQUIRE(smsRecInterface.GetCount() == 0);
-
- // Test updating record
- REQUIRE(smsRecInterface.Add(recordIN));
- recordIN.ID = 1;
- recordIN.body = bodyTest2;
- REQUIRE(smsRecInterface.Update(recordIN));
-
- auto record = smsRecInterface.GetByID(1);
- REQUIRE(record.ID != 0);
- REQUIRE(record.body == bodyTest2);
-
- // SMS database should contain 1 record
- REQUIRE(smsRecInterface.GetCount() == 1);
-
- // Remove existing record
- REQUIRE(smsRecInterface.RemoveByID(1));
- // SMS database should be empty
- REQUIRE(smsRecInterface.GetCount() == 0);
-
- // Test fetching record by using Thread ID
- // Add records with different numbers, they should be placed in separate threads&contacts dbs
- recordIN.body = bodyTest;
- recordIN.number = numberTest;
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.Add(recordIN));
-
- recordIN.number = numberTest2;
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.Add(recordIN));
-
- // Get all available records by specified thread ID and check for invalid data
- records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1");
- REQUIRE((*records).size() == 2);
- for (const auto &w : *records) {
- REQUIRE(w.body == bodyTest);
- REQUIRE(w.number == numberTest);
- }
+ // Remove records one by one
+ REQUIRE(smsRecInterface.RemoveByID(1));
+ REQUIRE(smsRecInterface.RemoveByID(2));
+
+ // SMS database should not contain any records
+ REQUIRE(smsRecInterface.GetCount() == 0);
+
+ // Test updating record
+ REQUIRE(smsRecInterface.Add(recordIN));
+ recordIN.ID = 1;
+ recordIN.body = bodyTest2;
+ REQUIRE(smsRecInterface.Update(recordIN));
+
+ auto record = smsRecInterface.GetByID(1);
+ REQUIRE(record.ID != 0);
+ REQUIRE(record.body == bodyTest2);
+
+ // SMS database should contain 1 record
+ REQUIRE(smsRecInterface.GetCount() == 1);
+
+ // Remove existing record
+ REQUIRE(smsRecInterface.RemoveByID(1));
+ // SMS database should be empty
+ REQUIRE(smsRecInterface.GetCount() == 0);
+
+ // Test fetching record by using Thread ID
+ // Add records with different numbers, they should be placed in separate threads&contacts dbs
+ recordIN.body = bodyTest;
+ recordIN.number = numberTest;
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+
+ recordIN.number = numberTest2;
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+
+ // Get all available records by specified thread ID and check for invalid data
+ records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1");
+ REQUIRE((*records).size() == 2);
+ for (const auto &w : *records) {
+ REQUIRE(w.body == bodyTest);
+ REQUIRE(w.number == numberTest);
+ }
- // Get all available records by specified thread ID and check for invalid data
- records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "2");
- REQUIRE((*records).size() == 2);
- for (const auto &w : *records) {
- REQUIRE(w.body == bodyTest);
- REQUIRE(w.number == numberTest2);
- }
+ // Get all available records by specified thread ID and check for invalid data
+ records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "2");
+ REQUIRE((*records).size() == 2);
+ for (const auto &w : *records) {
+ REQUIRE(w.body == bodyTest);
+ REQUIRE(w.number == numberTest2);
+ }
- // Get all available records by specified contact ID and check for invalid data
- records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
- REQUIRE((*records).size() == 2);
- for (const auto &w : *records) {
- REQUIRE(w.body == bodyTest);
- REQUIRE(w.number == numberTest);
- }
+ // Get all available records by specified contact ID and check for invalid data
+ records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
+ REQUIRE((*records).size() == 2);
+ for (const auto &w : *records) {
+ REQUIRE(w.body == bodyTest);
+ REQUIRE(w.number == numberTest);
+ }
- // Get all available records by specified contact ID and check for invalid data
- records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "2");
- REQUIRE((*records).size() == 2);
- for (const auto &w : *records) {
- REQUIRE(w.body == bodyTest);
- REQUIRE(w.number == numberTest2);
- }
+ // Get all available records by specified contact ID and check for invalid data
+ records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "2");
+ REQUIRE((*records).size() == 2);
+ for (const auto &w : *records) {
+ REQUIRE(w.body == bodyTest);
+ REQUIRE(w.number == numberTest2);
+ }
- // Remove sms records in order to check automatic management of threads and contact databases
- ThreadRecordInterface threadRecordInterface(smsDB.get(), contactsDB.get());
- REQUIRE(smsRecInterface.RemoveByID(1));
- records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
- REQUIRE((*records).size() == 1);
-
- REQUIRE(threadRecordInterface.GetCount() == 2);
-
- REQUIRE(smsRecInterface.RemoveByID(2));
- records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
- REQUIRE((*records).size() == 0);
- REQUIRE(threadRecordInterface.GetCount() == 1);
-
- REQUIRE(smsRecInterface.RemoveByID(3));
- REQUIRE(smsRecInterface.RemoveByID(4));
- REQUIRE(threadRecordInterface.GetCount() == 0);
-
- // Test removing a message which belongs to non-existent thread
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsDB->threads.removeById(1)); // stealthy thread remove
- REQUIRE(smsRecInterface.RemoveByID(1));
-
- // Test handling of missmatch in sms vs. thread tables
- auto trueCount = 10;
- // prepare
- for (auto added = 0; added < trueCount; added++) {
- recordIN.date = added; // for proper order
- recordIN.body = std::to_string(added + 1); // == ID
- REQUIRE(smsRecInterface.Add(recordIN)); // threadID = 1
- }
- ThreadRecord threadRec = threadRecordInterface.GetByID(1);
- REQUIRE(threadRec.isValid());
- ThreadsTableRow threadRaw{{.ID = threadRec.ID},
- .date = threadRec.date,
- .msgCount = threadRec.msgCount,
- .unreadMsgCount = threadRec.unreadMsgCount,
- .contactID = threadRec.contactID,
- .snippet = threadRec.snippet,
- .type = threadRec.type};
- threadRaw.msgCount = trueCount + 1; // break the DB
- REQUIRE(smsDB->threads.update(threadRaw));
-
- REQUIRE(static_cast<int>(smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1")->size()) ==
- trueCount);
- // end of preparation, now test
- for (auto latest = trueCount; latest > 0; latest--) {
- REQUIRE(smsRecInterface.RemoveByID(latest)); // remove the latest
- switch (latest) { // was just removed
- case 3: // remaining 2 or more
- default:
- REQUIRE(threadRecordInterface.GetByID(1).snippet.c_str() == std::to_string(latest - 1)); // next to newest
- break;
- case 2: // remaining 1
- REQUIRE(threadRecordInterface.GetByID(1).snippet.c_str() == std::to_string(1)); // only one remaining
- break;
- case 1: // no sms remaining
- // make sure there is no thread nor sms
- REQUIRE(threadRecordInterface.GetCount() == 0);
- REQUIRE(smsRecInterface.GetCount() == 0);
- break;
+ // Remove sms records in order to check automatic management of threads and contact databases
+ ThreadRecordInterface threadRecordInterface(smsDB.get(), contactsDB.get());
+ REQUIRE(smsRecInterface.RemoveByID(1));
+ records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
+ REQUIRE((*records).size() == 1);
+
+ REQUIRE(threadRecordInterface.GetCount() == 2);
+
+ REQUIRE(smsRecInterface.RemoveByID(2));
+ records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
+ REQUIRE((*records).size() == 0);
+ REQUIRE(threadRecordInterface.GetCount() == 1);
+
+ REQUIRE(smsRecInterface.RemoveByID(3));
+ REQUIRE(smsRecInterface.RemoveByID(4));
+ REQUIRE(threadRecordInterface.GetCount() == 0);
+
+ // Test removing a message which belongs to non-existent thread
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsDB->threads.removeById(1)); // stealthy thread remove
+ REQUIRE(smsRecInterface.RemoveByID(1));
+
+ // Test handling of missmatch in sms vs. thread tables
+ auto trueCount = 10;
+ // prepare
+ for (auto added = 0; added < trueCount; added++) {
+ recordIN.date = added; // for proper order
+ recordIN.body = std::to_string(added + 1); // == ID
+ REQUIRE(smsRecInterface.Add(recordIN)); // threadID = 1
+ }
+ ThreadRecord threadRec = threadRecordInterface.GetByID(1);
+ REQUIRE(threadRec.isValid());
+ ThreadsTableRow threadRaw{{.ID = threadRec.ID},
+ .date = threadRec.date,
+ .msgCount = threadRec.msgCount,
+ .unreadMsgCount = threadRec.unreadMsgCount,
+ .contactID = threadRec.contactID,
+ .snippet = threadRec.snippet,
+ .type = threadRec.type};
+ threadRaw.msgCount = trueCount + 1; // break the DB
+ REQUIRE(smsDB->threads.update(threadRaw));
+
+ REQUIRE(static_cast<int>(
+ smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1")->size()) == trueCount);
+ // end of preparation, now test
+ for (auto latest = trueCount; latest > 0; latest--) {
+ REQUIRE(smsRecInterface.RemoveByID(latest)); // remove the latest
+ switch (latest) { // was just removed
+ case 3: // remaining 2 or more
+ default:
+ REQUIRE(threadRecordInterface.GetByID(1).snippet.c_str() ==
+ std::to_string(latest - 1)); // next to newest
+ break;
+ case 2: // remaining 1
+ REQUIRE(threadRecordInterface.GetByID(1).snippet.c_str() == std::to_string(1)); // only one remaining
+ break;
+ case 1: // no sms remaining
+ // make sure there is no thread nor sms
+ REQUIRE(threadRecordInterface.GetCount() == 0);
+ REQUIRE(smsRecInterface.GetCount() == 0);
+ break;
+ }
}
+
+ // Test removing by field
+ recordIN.number = numberTest;
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.RemoveByField(SMSRecordField::ThreadID, "1"));
+ REQUIRE(smsRecInterface.GetCount() == 0);
+
+ recordIN.number = numberTest;
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.RemoveByField(SMSRecordField::ContactID, "1"));
+ REQUIRE(smsRecInterface.GetCount() == 0);
}
- // Test removing by field
- recordIN.number = numberTest;
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.RemoveByField(SMSRecordField::ThreadID, "1"));
- REQUIRE(smsRecInterface.GetCount() == 0);
-
- recordIN.number = numberTest;
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.Add(recordIN));
- REQUIRE(smsRecInterface.RemoveByField(SMSRecordField::ContactID, "1"));
- REQUIRE(smsRecInterface.GetCount() == 0);
+ SECTION("SMS Record Draft and Input test")
+ {
+ recordIN.type = SMSType ::INBOX;
+
+ // Add 3 INBOX Records
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+ REQUIRE(smsRecInterface.Add(recordIN));
+
+ // Add 1 Draft Records
+ recordIN.type = SMSType ::DRAFT;
+ recordIN.body = "Draft Message";
+ REQUIRE(smsRecInterface.Add(recordIN));
+
+ auto query = std::make_shared<db::query::SMSGetForList>(1, 0, 100);
+ auto ret = smsRecInterface.runQuery(query);
+ auto result = dynamic_cast<db::query::SMSGetForListResult *>(ret.get());
+ REQUIRE(result != nullptr);
+
+ REQUIRE(result->getCount() == 3);
+ REQUIRE(result->getResults().size() == 4);
+ REQUIRE(result->getResults().back().type == SMSType::INPUT);
+ REQUIRE(result->getDraft().type == SMSType::DRAFT);
+ }
Database::deinitialize();
}
M module-db/tests/SMSTable_tests.cpp => module-db/tests/SMSTable_tests.cpp +74 -56
@@ 1,13 1,3 @@
-
-/*
- * @file smsdb.sms_tests.cpp
- * @author Mateusz Piesta (mateusz.piesta@mudita.com)
- * @date 28.05.19
- * @brief
- * @copyright Copyright (C) 2019 mudita.com
- * @details
- */
-
#include "vfs.hpp"
#include <catch2/catch.hpp>
@@ 18,9 8,6 @@
#include "Tables/SettingsTable.hpp"
#include <algorithm>
-
-#include <cstdint>
-#include <cstdio>
#include <cstring>
TEST_CASE("SMS Table tests")
@@ 43,64 30,95 @@ TEST_CASE("SMS Table tests")
};
- // add 4 elements into table
- REQUIRE(smsdb.sms.add(testRow1));
- REQUIRE(smsdb.sms.add(testRow1));
- REQUIRE(smsdb.sms.add(testRow1));
- REQUIRE(smsdb.sms.add(testRow1));
+ SMSTableRow testRow2 = {{.ID = 0},
+ .threadID = 0,
+ .contactID = 0,
+ .date = 0,
+ .dateSent = 0,
+ .errorCode = 0,
+ .body = "Test Draft SMS",
+ .type = SMSType ::DRAFT
+
+ };
+
+ SECTION("SMS Table test")
+ {
+ // add 4 elements into table
+ REQUIRE(smsdb.sms.add(testRow1));
+ REQUIRE(smsdb.sms.add(testRow1));
+ REQUIRE(smsdb.sms.add(testRow1));
+ REQUIRE(smsdb.sms.add(testRow1));
+
+ // Table should have 4 elements
+ REQUIRE(smsdb.sms.count() == 4);
+
+ // update existing element in table
+ testRow1.ID = 4;
+ testRow1.body = "updated Test SMS message ";
+ REQUIRE(smsdb.sms.update(testRow1));
+
+ // Get table row using valid ID & check if it was updated
+ auto sms = smsdb.sms.getById(4);
+ REQUIRE(sms.body == testRow1.body);
+
+ // Get table row using invalid ID(should return empty smsdb.smsRow)
+ auto smsFailed = smsdb.sms.getById(100);
+ REQUIRE(smsFailed.body == "");
+
+ // Get table rows using valid offset/limit parameters
+ auto retOffsetLimit = smsdb.sms.getLimitOffset(0, 4);
+ REQUIRE(retOffsetLimit.size() == 4);
- // Table should have 4 elements
- REQUIRE(smsdb.sms.count() == 4);
+ // Get table rows using valid offset/limit parameters and specific field's ID
+ REQUIRE(smsdb.sms.getLimitOffsetByField(0, 4, SMSTableFields::Date, "0").size() == 4);
- // update existing element in table
- testRow1.ID = 4;
- testRow1.body = "updated Test SMS message ";
- REQUIRE(smsdb.sms.update(testRow1));
+ // Get table rows using invalid limit parameters(should return 4 elements instead of 100)
+ auto retOffsetLimitBigger = smsdb.sms.getLimitOffset(0, 100);
+ REQUIRE(retOffsetLimitBigger.size() == 4);
- // Get table row using valid ID & check if it was updated
- auto sms = smsdb.sms.getById(4);
- REQUIRE(sms.body == testRow1.body);
+ // Get table rows using invalid offset/limit parameters(should return empty object)
+ auto retOffsetLimitFailed = smsdb.sms.getLimitOffset(5, 4);
+ REQUIRE(retOffsetLimitFailed.size() == 0);
- // Get table row using invalid ID(should return empty smsdb.smsRow)
- auto smsFailed = smsdb.sms.getById(100);
- REQUIRE(smsFailed.body == "");
+ // Get count of elements by field's ID
+ REQUIRE(smsdb.sms.countByFieldId("thread_id", 0) == 4);
- // Get table rows using valid offset/limit parameters
- auto retOffsetLimit = smsdb.sms.getLimitOffset(0, 4);
- REQUIRE(retOffsetLimit.size() == 4);
+ // Get count of elements by invalid field's ID
+ REQUIRE(smsdb.sms.countByFieldId("invalid_field", 0) == 0);
- // Get table rows using valid offset/limit parameters and specific field's ID
- REQUIRE(smsdb.sms.getLimitOffsetByField(0, 4, SMSTableFields::Date, "0").size() == 4);
+ REQUIRE(smsdb.sms.removeById(2));
- // Get table rows using invalid limit parameters(should return 4 elements instead of 100)
- auto retOffsetLimitBigger = smsdb.sms.getLimitOffset(0, 100);
- REQUIRE(retOffsetLimitBigger.size() == 4);
+ // Table should have now 3 elements
+ REQUIRE(smsdb.sms.count() == 3);
- // Get table rows using invalid offset/limit parameters(should return empty object)
- auto retOffsetLimitFailed = smsdb.sms.getLimitOffset(5, 4);
- REQUIRE(retOffsetLimitFailed.size() == 0);
+ // Remove non existing element
+ REQUIRE(smsdb.sms.removeById(100));
- // Get count of elements by field's ID
- REQUIRE(smsdb.sms.countByFieldId("thread_id", 0) == 4);
+ // Remove all elements from table
+ REQUIRE(smsdb.sms.removeById(1));
+ REQUIRE(smsdb.sms.removeById(3));
+ REQUIRE(smsdb.sms.removeById(4));
- // Get count of elements by invalid field's ID
- REQUIRE(smsdb.sms.countByFieldId("invalid_field", 0) == 0);
+ // Table should be empty now
+ REQUIRE(smsdb.sms.count() == 0);
+ }
- REQUIRE(smsdb.sms.removeById(2));
+ SECTION("SMS Draft and Input Table test")
+ {
+ REQUIRE(smsdb.sms.add(testRow1));
+ REQUIRE(smsdb.sms.add(testRow1));
+ REQUIRE(smsdb.sms.add(testRow2));
- // Table should have now 3 elements
- REQUIRE(smsdb.sms.count() == 3);
+ REQUIRE(smsdb.sms.countWithoutDraftsByThreadId(0) == 2);
+ REQUIRE(smsdb.sms.count() == 3);
- // Remove non existing element
- REQUIRE(smsdb.sms.removeById(100));
+ REQUIRE(smsdb.sms.getDraftByThreadId(0).body == "Test Draft SMS");
- // Remove all elements from table
- REQUIRE(smsdb.sms.removeById(1));
- REQUIRE(smsdb.sms.removeById(3));
- REQUIRE(smsdb.sms.removeById(4));
+ auto results = smsdb.sms.getByThreadIdWithoutDraftWithEmptyInput(0, 0, 10);
- // Table should be empty now
- REQUIRE(smsdb.sms.count() == 0);
+ REQUIRE(results.size() == 3);
+ REQUIRE(results.back().type == SMSType ::INPUT);
+ }
Database::deinitialize();
}
M module-gui/gui/widgets/BoxLayout.cpp => module-gui/gui/widgets/BoxLayout.cpp +16 -0
@@ 3,6 3,7 @@
#include <InputEvent.hpp>
#include <Label.hpp>
#include <log/log.hpp>
+#include "assert.h"
namespace gui
{
@@ 135,11 136,22 @@ namespace gui
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;
+ }
+
void BoxLayout::addToOutOfDrawAreaList(Item *it)
{
if (it->visible) {
@@ 452,6 464,10 @@ namespace gui
resizeItems(); // vs mark dirty
setNavigation();
+ if (parentOnRequestedResizeCallback != nullptr) {
+ parentOnRequestedResizeCallback();
+ }
+
return granted;
}
M module-gui/gui/widgets/BoxLayout.hpp => module-gui/gui/widgets/BoxLayout.hpp +5 -1
@@ 101,11 101,15 @@ namespace gui
/// set navigation from last to fist element in box
virtual void setNavigation();
std::list<Item *>::iterator getNavigationFocusedItem();
- unsigned int getFocusItemIndex() const;
+ [[nodiscard]] unsigned int getFocusItemIndex() const;
+ [[nodiscard]] unsigned int getVisibleChildrenCount();
+ /// If requested causes box to change its structure parent may need to do some actions via this callback.
+ std::function<void()> parentOnRequestedResizeCallback = nullptr;
void setVisible(bool value) override;
/// set visible but from previous scope... (page, element etc)
void setVisible(bool value, bool previous);
void setReverseOrder(bool value);
+ [[nodiscard]] bool getReverseOrder();
/// callback for situaton when we reached top/bottom/left/right of box
/// if we want to do sth special (i.e. request new items)
std::function<bool(const InputEvent &inputEvent)> borderCallback = nullptr;
M module-gui/gui/widgets/ListView.cpp => module-gui/gui/widgets/ListView.cpp +115 -41
@@ 17,14 17,17 @@ namespace gui
activeItem = false;
}
- bool ListViewScroll::shouldShowScroll(int currentPageSize, int elementsCount)
+ bool ListViewScroll::shouldShowScroll(unsigned int currentPageSize, unsigned int elementsCount)
{
return ((parent->widgetArea.w > style::listview::scroll::min_space) &&
(parent->widgetArea.h > style::listview::scroll::min_space) && currentPageSize < elementsCount);
}
- void ListViewScroll::update(int startIndex, int currentPageSize, int elementsCount, int topMargin)
+ void ListViewScroll::update(unsigned int startIndex,
+ unsigned int currentPageSize,
+ unsigned int elementsCount,
+ int topMargin)
{
if (shouldShowScroll(currentPageSize, elementsCount)) {
@@ 64,8 67,8 @@ namespace gui
this->setBorderColor(ColorNoColor);
body = new VBox{this, 0, 0, w, h};
- body->setBorderColor(ColorNoColor);
body->setAlignment(Alignment::Vertical::Top);
+ body->setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
body->borderCallback = [this](const InputEvent &inputEvent) -> bool {
if (inputEvent.state != InputEvent::State::keyReleasedShort) {
@@ 82,6 85,11 @@ namespace gui
}
};
+ body->parentOnRequestedResizeCallback = [this]() {
+ if (pageLoaded)
+ recalculateOnBoxRequestedResize();
+ };
+
scroll = new ListViewScroll(this,
style::listview::scroll::x,
style::listview::scroll::y,
@@ 113,36 121,51 @@ namespace gui
scrollTopMargin = value;
}
+ void ListView::setOrientation(style::listview::Orientation value)
+ {
+ orientation = value;
+
+ if (orientation == style::listview::Orientation::TopBottom) {
+ body->setAlignment(Alignment::Vertical::Top);
+ }
+ else {
+ body->setAlignment(Alignment::Vertical::Bottom);
+ }
+ }
+
void ListView::setProvider(std::shared_ptr<ListItemProvider> prov)
{
if (prov != nullptr) {
provider = prov;
provider->list = this;
-
- rebuildList();
}
}
- void ListView::rebuildList(style::listview::RebuildType rebuildType, unsigned int dataOffset)
+ void ListView::rebuildList(style::listview::RebuildType rebuildType, unsigned int dataOffset, bool forceRebuild)
{
- setElementsCount(provider->requestRecordsCount());
+ if (pageLoaded || forceRebuild) {
+ setElementsCount(provider->requestRecordsCount());
- setup(rebuildType, dataOffset);
- clearItems();
+ setup(rebuildType, dataOffset);
+ clearItems();
- // If deletion operation caused last page to be removed request previous one.
- if (startIndex != 0 && startIndex == elementsCount) {
- requestPreviousPage();
+ // If deletion operation caused last page to be removed request previous one.
+ if ((startIndex != 0 && startIndex == elementsCount)) {
+ requestPreviousPage();
+ }
+ else {
+ provider->requestRecords(startIndex, calculateLimit());
+ }
}
else {
- provider->requestRecords(startIndex, calculateLimit());
+ rebuildRequests.push_front({rebuildType, dataOffset});
}
};
void ListView::setup(style::listview::RebuildType rebuildType, unsigned int dataOffset)
{
if (rebuildType == style::listview::RebuildType::Full) {
- startIndex = 0;
+ setStartIndex();
storedFocusIndex = 0;
}
else if (rebuildType == style::listview::RebuildType::OnOffset) {
@@ 155,6 178,7 @@ namespace gui
}
}
else if (rebuildType == style::listview::RebuildType::InPlace) {
+
storedFocusIndex = body->getFocusItemIndex();
if (direction == style::listview::Direction::Top) {
@@ 181,9 205,9 @@ namespace gui
void ListView::clear()
{
clearItems();
+ setStartIndex();
body->setReverseOrder(false);
- startIndex = 0;
- direction = style::listview::Direction::Bottom;
+ direction = style::listview::Direction::Bottom;
}
void ListView::clearItems()
@@ 222,6 246,12 @@ namespace gui
resizeWithScroll();
checkFirstPage();
pageLoaded = true;
+
+ // Check if there were queued rebuild Requests - if so rebuild list again.
+ if (!rebuildRequests.empty()) {
+ rebuildList(rebuildRequests.back().first, rebuildRequests.back().second);
+ rebuildRequests.pop_back();
+ }
}
void ListView::onProviderDataUpdate()
@@ 237,24 267,41 @@ namespace gui
return Order::Previous;
}
+ void ListView::setStartIndex()
+ {
+ if (orientation == style::listview::Orientation::TopBottom) {
+ startIndex = 0;
+ }
+ else {
+ startIndex = elementsCount;
+ }
+ }
+
void ListView::recalculateStartIndex()
{
if (direction == style::listview::Direction::Top) {
- startIndex = startIndex - currentPageSize > 0 ? startIndex - currentPageSize : 0;
+
+ startIndex = startIndex < currentPageSize ? 0 : startIndex - currentPageSize;
}
}
void ListView::checkFirstPage()
{
- // On direction top check if first page is filled with items. If not reload page with direction bottom so it
- // will be filled.
- if (direction == style::listview::Direction::Top && startIndex == 0) {
+ // Check if first page is filled with items. If not reload page to be filed with items. Check for both
+ // Orientations.
+ if (orientation == style::listview::Orientation::TopBottom && direction == style::listview::Direction::Top &&
+ startIndex == 0) {
if (body->getSizeLeft() > provider->getMinimalItemHeight()) {
- clearItems();
- body->setReverseOrder(false);
- direction = style::listview::Direction::Bottom;
focusOnLastItem = true;
- provider->requestRecords(startIndex, calculateLimit());
+ rebuildList();
+ }
+ }
+
+ if (orientation == style::listview::Orientation::BottomTop && direction == style::listview::Direction::Bottom &&
+ startIndex + currentPageSize == elementsCount) {
+ if (body->getSizeLeft() > provider->getMinimalItemHeight()) {
+ focusOnLastItem = true;
+ rebuildList();
}
}
}
@@ 298,8 345,6 @@ namespace gui
if (!body->setFocusOnElement(storedFocusIndex)) {
body->setFocusOnLastElement();
}
-
- storedFocusIndex = 0;
}
if (focusOnLastItem) {
@@ 321,11 366,40 @@ namespace gui
bool ListView::onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim)
{
Rect::onDimensionChanged(oldDim, newDim);
- body->setSize(newDim.w, newDim.h);
- refresh();
+ body->setSize(body->getWidth(), newDim.h);
+ scroll->update(startIndex, currentPageSize, elementsCount, scrollTopMargin);
+
return true;
}
+ auto ListView::handleRequestResize([[maybe_unused]] const Item *child,
+ unsigned short request_w,
+ unsigned short request_h) -> Size
+ {
+
+ return Size(request_w, request_h);
+ }
+
+ void ListView::recalculateOnBoxRequestedResize()
+ {
+ if (currentPageSize != body->getVisibleChildrenCount()) {
+
+ unsigned int diff = currentPageSize < body->getVisibleChildrenCount()
+ ? 0
+ : currentPageSize - body->getVisibleChildrenCount();
+ currentPageSize = body->getVisibleChildrenCount();
+
+ if (direction == style::listview::Direction::Top) {
+ startIndex += diff;
+ }
+ else {
+ startIndex = startIndex < diff ? 0 : startIndex - diff;
+ }
+
+ checkFirstPage();
+ }
+ }
+
bool ListView::onInput(const InputEvent &inputEvent)
{
return body->onInput(inputEvent);
@@ 339,11 413,11 @@ namespace gui
return count;
}
- unsigned int ListView::calculateLimit()
+ unsigned int ListView::calculateLimit(style::listview::Direction value)
{
auto minLimit =
(2 * currentPageSize > calculateMaxItemsOnPage() ? 2 * currentPageSize : calculateMaxItemsOnPage());
- if (direction == style::listview::Direction::Bottom)
+ if (value == style::listview::Direction::Bottom)
return (minLimit + startIndex <= elementsCount ? minLimit : elementsCount - startIndex);
else
return minLimit < startIndex ? minLimit : startIndex;
@@ 351,16 425,13 @@ namespace gui
bool ListView::requestNextPage()
{
- direction = style::listview::Direction::Bottom;
- body->setReverseOrder(false);
-
if (startIndex + currentPageSize >= elementsCount && boundaries == style::listview::Boundaries::Continuous) {
startIndex = 0;
}
else if (startIndex + currentPageSize >= elementsCount && boundaries == style::listview::Boundaries::Fixed) {
- return true;
+ return false;
}
else {
@@ 368,6 439,8 @@ namespace gui
: elementsCount - (elementsCount - startIndex);
}
+ direction = style::listview::Direction::Bottom;
+ body->setReverseOrder(false);
pageLoaded = false;
provider->requestRecords(startIndex, calculateLimit());
@@ 376,8 449,6 @@ namespace gui
bool ListView::requestPreviousPage()
{
- direction = style::listview::Direction::Top;
- body->setReverseOrder(true);
auto topFetchIndex = 0;
auto limit = 0;
@@ 385,22 456,25 @@ namespace gui
topFetchIndex = elementsCount - (elementsCount % currentPageSize);
startIndex = elementsCount;
- limit = calculateLimit() - topFetchIndex;
+ limit = calculateLimit(style::listview::Direction::Top) - topFetchIndex;
}
else if (startIndex == 0 && boundaries == style::listview::Boundaries::Fixed) {
- return true;
+ return false;
}
else {
- limit = calculateLimit();
- topFetchIndex = startIndex - calculateLimit() > 0 ? startIndex - calculateLimit() : 0;
+ limit = calculateLimit(style::listview::Direction::Top);
+ topFetchIndex = startIndex < calculateLimit(style::listview::Direction::Top)
+ ? 0
+ : startIndex - calculateLimit(style::listview::Direction::Top);
}
+ direction = style::listview::Direction::Top;
+ body->setReverseOrder(true);
pageLoaded = false;
provider->requestRecords(topFetchIndex, limit);
return true;
}
-
} /* namespace gui */
M module-gui/gui/widgets/ListView.hpp => module-gui/gui/widgets/ListView.hpp +14 -7
@@ 16,8 16,8 @@ namespace gui
public:
ListViewScroll(Item *parent, uint32_t x, uint32_t y, uint32_t w, uint32_t h);
- bool shouldShowScroll(int listPageSize, int elementsCount);
- void update(int startIndex, int listPageSize, int elementsCount, int topMargin);
+ bool shouldShowScroll(unsigned int listPageSize, unsigned int elementsCount);
+ void update(unsigned int startIndex, unsigned int listPageSize, unsigned int elementsCount, int topMargin);
};
class ListView : public Rect
@@ 29,14 29,16 @@ namespace gui
std::shared_ptr<ListItemProvider> provider = nullptr;
VBox *body = nullptr;
ListViewScroll *scroll = nullptr;
+ std::list<std::pair<style::listview::RebuildType, unsigned int>> rebuildRequests;
unsigned int currentPageSize = 0;
- bool pageLoaded = false;
+ bool pageLoaded = true;
bool focusOnLastItem = false;
int scrollTopMargin = style::margins::big;
- style::listview::Boundaries boundaries = style::listview::Boundaries::Fixed;
- style::listview::Direction direction = style::listview::Direction::Bottom;
+ style::listview::Boundaries boundaries = style::listview::Boundaries::Fixed;
+ style::listview::Direction direction = style::listview::Direction::Bottom;
+ style::listview::Orientation orientation = style::listview::Orientation::TopBottom;
void clearItems();
virtual void addItemsOnPage();
@@ 45,8 47,10 @@ namespace gui
void resizeWithScroll();
void recalculateStartIndex();
void checkFirstPage();
+ void setStartIndex();
+ void recalculateOnBoxRequestedResize();
unsigned int calculateMaxItemsOnPage();
- unsigned int calculateLimit();
+ unsigned int calculateLimit(style::listview::Direction value = style::listview::Direction::Bottom);
Order getOrderFromDirection();
virtual bool requestNextPage();
virtual bool requestPreviousPage();
@@ 60,9 64,11 @@ namespace gui
void setElementsCount(unsigned int count);
void setProvider(std::shared_ptr<ListItemProvider> provider);
void rebuildList(style::listview::RebuildType rebuildType = style::listview::RebuildType::Full,
- unsigned int dataOffset = 0);
+ unsigned int dataOffset = 0,
+ bool forceRebuild = false);
void clear();
std::shared_ptr<ListItemProvider> getProvider();
+ void setOrientation(style::listview::Orientation value);
void setBoundaries(style::listview::Boundaries value);
void setScrollTopMargin(int value);
void setAlignment(const Alignment &value) override;
@@ 72,6 78,7 @@ namespace gui
std::list<DrawCommand *> buildDrawList() override;
bool onInput(const InputEvent &inputEvent) override;
bool onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) override;
+ auto handleRequestResize(const Item *, unsigned short request_w, unsigned short request_h) -> Size override;
};
} /* namespace gui */
M module-gui/gui/widgets/Rect.hpp => module-gui/gui/widgets/Rect.hpp +1 -1
@@ 31,7 31,7 @@ namespace gui
/// mark if we japs should be painted. small protrusions indicating a speech bubble
RectangleYapFlags yaps = {RectangleYapFlags::GUI_RECT_YAP_NO_YAPS};
/// yap size in horizontal width.
- unsigned short yapSize = {style::window::messages::yaps_size_default};
+ unsigned short yapSize = style::window::default_rect_yaps;
public:
Rect();
M module-gui/gui/widgets/Style.hpp => module-gui/gui/widgets/Style.hpp +9 -25
@@ 37,6 37,7 @@ namespace style
const inline std::string medium = "gt_pressura_regular_24";
}; // namespace font
}; // namespace footer
+
namespace window
{
const inline uint32_t default_left_margin = 20;
@@ 48,6 49,7 @@ namespace style
const inline uint32_t default_border_focus_w = 2;
const inline uint32_t default_border_rect_no_focus = 1;
const inline uint32_t default_border_no_focus_w = 0;
+ const inline uint32_t default_rect_yaps = 10;
namespace font
{
const inline std::string supersizemelight = "gt_pressura_light_90";
@@ 79,32 81,8 @@ namespace style
/// minimal label decoration for Option
void decorateOption(gui::Label *el);
- namespace messages
- {
- inline const uint32_t sms_radius = 7;
- inline const uint32_t sms_border_no_focus = 1;
- inline const uint32_t sms_thread_item_h = 100;
- const inline unsigned short yaps_size_default = 10;
- const inline gui::Length sms_max_width = 320;
- const inline unsigned short sms_h_padding = 15;
- const inline unsigned short sms_h_left_padding = 25;
- const inline unsigned short sms_v_padding = 10;
- const inline unsigned short sms_vertical_spacer = 10;
- const inline unsigned short new_sms_vertical_spacer = 25;
- const inline unsigned short sms_failed_offset = 39;
- const inline unsigned short sms_error_icon_offset = 2;
- const inline gui::Padding sms_left_bubble_padding =
- gui::Padding(style::window::messages::sms_h_left_padding,
- style::window::messages::sms_v_padding,
- style::window::messages::sms_h_padding,
- style::window::messages::sms_v_padding);
- const inline gui::Padding sms_right_bubble_padding = gui::Padding(style::window::messages::sms_h_padding,
- style::window::messages::sms_v_padding,
- style::window::messages::sms_h_padding,
- style::window::messages::sms_v_padding);
- } // namespace messages
-
}; // namespace window
+
namespace settings
{
namespace date
@@ 216,6 194,12 @@ namespace style
///< offset.
};
+ enum class Orientation
+ {
+ TopBottom,
+ BottomTop
+ };
+
namespace scroll
{
const inline uint32_t x = 0;
M module-gui/gui/widgets/Text.cpp => module-gui/gui/widgets/Text.cpp +8 -6
@@ 333,8 333,9 @@ namespace gui
: area(Area::Max).h;
}
- Length w = sizeMinusPadding(Axis::X, Area::Max);
- Length h = area(Area::Normal).size(Axis::Y);
+ Length w = sizeMinusPadding(Axis::X, Area::Max);
+ Length h = sizeMinusPadding(Axis::Y, Area::Max);
+
auto line_y_position = padding.top;
auto cursor = 0;
@@ 393,12 394,13 @@ namespace gui
// should be done on each loop
{
uint16_t h_used = line_y_position + padding.bottom;
- uint16_t w_used = lines.maxWidth();
+ uint16_t w_used = lines.maxWidth() + padding.getSumInAxis(Axis::X);
+
if (lines.size() == 0) {
debug_text("No lines to show, try to at least fit in cursor");
- if (format.getFont() != nullptr) {
- h_used += format.getFont()->info.line_height;
- w_used += TextCursor::default_width;
+ if (format.getFont() != nullptr && line_y_position < format.getFont()->info.line_height) {
+ h_used = format.getFont()->info.line_height;
+ w_used = TextCursor::default_width;
debug_text("empty line height: %d", h_used);
}
}
M module-gui/test/test-google/test-gui-listview.cpp => module-gui/test/test-google/test-gui-listview.cpp +1 -0
@@ 51,6 51,7 @@ class ListViewTesting : public ::testing::Test
ASSERT_EQ(0, testListView->currentPageSize) << "List should be empty";
testListView->setProvider(testProvider);
+ testListView->rebuildList();
}
void TearDown() override