AProperty#
Observable container of T
.
Header: | #include <AUI/Common/AProperty.h> |
CMake: | aui_link(my_target PUBLIC aui::core) |
Detailed Description#
Experimental Feature
This API is experimental. Experimental APIs are likely to contain bugs, might be changed or removed in the future.
AProperty<T>
is a container holding an instance of T
. You can assign a value to it with operator=
and read
value with value()
method or implicit conversion operator T()
.
See property system for usage examples.
Declaration#
To declare a property inside your data model, use AProperty template:
LogObserver() { ON_CALL(*this, log(testing::_)).WillByDefault( { }); }AProperty<T>
is a container holding an instance of T
. You can assign a value to it with operator=
and read
value with value()
method or implicit conversion operator T()
.
AProperty behaves like a class/struct data member:
Non-const operators have side effects; const operators don't, so you can perform seamlessly:User u;
u.name = "Hello";
AString helloWorld = u.name + " world";
EXPECT_EQ(helloWorld, "Hello world");
auto doSomethingWithName = [](const AString& name) { EXPECT_EQ(name, "Hello"); };
User u;
u.name = "Hello";
doSomethingWithName(u.name);
Observing changes#
All property types offer .changed
field which is a signal reporting value changes. Let's make little observer
object for demonstration:
class LogObserver : public AObject {
public:
void log(const AString& msg) {
ALogger::info("LogObserver") << "Received value: " << msg;
}
};
Example usage:
auto observer = _new<LogObserver>();
auto u = aui::ptr::manage_shared(new User { .name = "Chloe" });
AObject::connect(u->name.changed, AUI_SLOT(observer)::log);
At the moment, the program prints nothing. When we change the property:
Code produces the following output:As you can see, observer received the update. But, for example, if we would like to display the value via
label, the label wouldn't display the current value until the next update. We want the label to display
current value without requiring an update. To do this, connect to the property directly, without explicitly
asking for changed
:
auto observer = _new<LogObserver>();
auto u = aui::ptr::manage_shared(new User { .name = "Chloe" });
EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(u->name, AUI_SLOT(observer)::log);
LogObserver::log
is made by AObject::connect
itself. In this document, we will call this behaviour as
"pre-fire".
Subsequent changes to field would send updates as well:
EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
u->name = "Marinette";
Whole program output when connecting to property directly:
[07:58:59][][LogObserver][INFO]: Received value: Chloe
[07:58:59][][LogObserver][INFO]: Received value: Marinette
Copy constructing AProperty#
Copying AProperty
is considered as a valid operation as it's a data holder. However, it's worth to note
that AProperty
copies it's underlying data field only, the signal-slot relations are not borrowed.
auto observer = _new<LogObserver>();
auto original = aui::ptr::manage_shared(new User { .name = "Chloe" });
EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(original->name, AUI_SLOT(observer)::log);
origin->name
and check that observer received an update, but value in the copy
remains:
EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
original->name = "Marinette";
EXPECT_EQ(copy->name, "Chloe"); // still
"Chloe"
-> "Marinette"
. The copy is not aware because
it is a copy. If we try to change the copy
's name:
The observer is not aware about changes in copy
. In fact. copy->name
has zero connections.
Copy assigning AProperty#
The situation with copy assigning auto copy = _new<User>(); *copy = *original;
is similar to copy
construction auto copy = _new<User>(*original);
, except that we are copying to some pre-existing
data structure that potentially have signal-skit relations already. So, not only connections should be kept
as is but a notification for copy destination's observers is needed.
As with copy construction, copy operation of AProperty
does not affect signal-slot relations. Moreover,
it notifies the observers.
auto observer = _new<LogObserver>();
auto original = aui::ptr::manage_shared(new User { .name = "Chloe" });
EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(original->name, AUI_SLOT(observer)::log);
EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
User copy { .name = "Marinette" };
*original = copy;
Moving AProperty#
Similary to copy, AProperty is both move assignable and constructible except that underlying value is moved instead of copying. Also, the observers of the source object receive notification that the value was emptied. The signal-slot relations are left unchanged.
auto observer = _new<LogObserver>();
auto original = aui::ptr::manage_shared(new User { .name = "Chloe" });
EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(original->name, AUI_SLOT(observer)::log);
// by move operation, we've affected the source, hence the
// empty string notification
EXPECT_CALL(*observer, log(AString(""))).Times(1);
auto moved = _new<User>(std::move(*original));
EXPECT_EQ(original->name, ""); // empty
EXPECT_EQ(moved->name, "Chloe"); // moved name
origin->name
and check that observer received an update, but value in the moved
remains:
EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
original->name = "Marinette";
EXPECT_EQ(moved->name, "Chloe"); // still
"Chloe"
-> ""
-> "Marinette"
. The moved
is not aware.
If we try to change the moved
's name:
The observer is not aware about changes in moved
. In fact. moved->name
has zero connections.
Move assignment work in a similar way to copy assignment:
auto observer = _new<LogObserver>();
auto original = aui::ptr::manage_shared(new User { .name = "Chloe" });
EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(original->name, AUI_SLOT(observer)::log);
EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
User copy { .name = "Marinette" };
*original = std::move(copy);
Non-const operators#
Refer to aui::PropertyModifier
.
AProperty and AVector#
Assuming you have an AVector wrapped with AProperty:
You can use square brackets to access items transparently: For ranged for loop, you need to dereferenceints
:
To modify the vector, you need to use writeScope()
:
ints.writeScope()->push_back(2);
// or
ints.writeScope() << 2;
// or
ints << 2; // implies writeScope()
Examples#
examples/app/game_of_life/src/main.cpp
Game of Life - Game of Life implementation that uses advanced large dynamic data rendering techniques such as ITexture, AImage to be GPU friendly. The computation is performed in AThreadPool.
examples/ui/contacts/src/view/ContactDetailsView.cpp
AUI Contacts - Usage of AUI_DECLARATIVE_FOR to make a contacts-like application.
examples/7guis/flight_booker/src/main.cpp
7GUIs Flight Booker - Flight Booker.
constexpr auto REGEX_DATE = ctre::match<"([0-9]+)\\.([0-9]+)\\.([0-9]{4})">;
struct DateTextFieldState {
AProperty<AOptional<system_clock::time_point>> parsed;
};
auto formatDate(system_clock::time_point date) { return "{0:%d}.{0:%m}.{0:%G}"_format(date); }
auto dateTextField(DateTextFieldState& state) {
examples/7guis/cells/src/Cell.h
7GUIs Cells - Spreadsheet processor (Excel).
examples/7guis/circle_drawer/src/main.cpp
7GUIs Circle Drawer - Undo, redo, dialog control.
examples/7guis/timer/src/main.cpp
7GUIs Timer - Timer example.
private:
_<ATimer> mTimer = _new<ATimer>(100ms);
high_resolution_clock::time_point mStartTime = high_resolution_clock::now();
AProperty<high_resolution_clock::time_point> mCurrentTime;
AProperty<high_resolution_clock::duration> mDuration = 30s;
APropertyPrecomputed<high_resolution_clock::duration> mElapsedTime = [&] {
return std::min(mCurrentTime - mStartTime, *mDuration);
};
examples/ui/contacts/src/view/ContactDetailsView.h
AUI Contacts - Usage of AUI_DECLARATIVE_FOR to make a contacts-like application.
examples/ui/contacts/src/model/Contact.h
AUI Contacts - Usage of AUI_DECLARATIVE_FOR to make a contacts-like application.
examples/7guis/cells/src/main.cpp
7GUIs Cells - Spreadsheet processor (Excel).
Public fields and Signals#
changed#
emits<T> changed
Signal that notifies data changes.
raw#
T raw
Stored value.
This field stores AProperty's wrapped value. It is not recommended to use this in casual use; although there are
might be an use case to modify the value without notifying. You can use notify()
to send a change notification.
Use at your own risk.
Examples#
examples/app/notes/CMakeLists.txt
Notes App - Note taking app that demonstrates usage of AListModel, AProperty, user data saving and loading.
# Uncomment this code to pull AUI:
#
# file(
# DOWNLOAD
# https://raw.githubusercontent.com/aui-framework/aui/master/aui.boot.cmake
# ${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
# include(${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
#
# auib_import(aui https://github.com/aui-framework/aui
# COMPONENTS core views json)
examples/app/game_of_life/CMakeLists.txt
Game of Life - Game of Life implementation that uses advanced large dynamic data rendering techniques such as ITexture, AImage to be GPU friendly. The computation is performed in AThreadPool.
# Uncomment this code to pull AUI:
#
# file(
# DOWNLOAD
# https://raw.githubusercontent.com/aui-framework/aui/master/aui.boot.cmake
# ${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
# include(${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
#
# auib_import(aui https://github.com/aui-framework/aui
# COMPONENTS core views json)
examples/ui/views/CMakeLists.txt
Views Example - All-in-one views building example.
# Uncomment this code to pull AUI:
#
# file(
# DOWNLOAD
# https://raw.githubusercontent.com/aui-framework/aui/master/aui.boot.cmake
# ${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
# include(${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
#
# auib_import(aui https://github.com/aui-framework/aui
# COMPONENTS core views audio curl)
examples/basic/hello_world/CMakeLists.txt
Console Hello World Example - Basic CLI Hello World application.
# Use AUI.Boot
file(
DOWNLOAD
https://raw.githubusercontent.com/aui-framework/aui/master/aui.boot.cmake
${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
include(${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
# link AUI
auib_import(
Public Methods#
biProjected#
Makes a bidirectional projection of this property.
Examples#
examples/7guis/flight_booker/src/main.cpp
7GUIs Flight Booker - Flight Booker.
auto dateTextField(DateTextFieldState& state) {
return _new<ATextField>() AUI_LET {
AObject::biConnect(
state.parsed.biProjected(aui::lambda_overloaded {
[](const AOptional<system_clock::time_point>& v) -> AString {
if (!v) {
return "";
}
return formatDate(*v);
examples/7guis/timer/src/main.cpp
7GUIs Timer - Timer example.
}),
Horizontal {
Label { "Duration:" },
_new<ASlider>() AUI_LET {
it&& mDuration.biProjected(aui::lambda_overloaded {
[](high_resolution_clock::duration d) -> aui::float_within_0_1 {
return float(d.count()) / float(MAX_DURATION.count());
},
[](aui::float_within_0_1 d) -> high_resolution_clock::duration {
return high_resolution_clock::duration(long(float(d) * float(MAX_DURATION.count())));
Makes a bidirectional projection of this property (by a single aui::lambda_overloaded).
Examples#
examples/7guis/flight_booker/src/main.cpp
7GUIs Flight Booker - Flight Booker.
auto dateTextField(DateTextFieldState& state) {
return _new<ATextField>() AUI_LET {
AObject::biConnect(
state.parsed.biProjected(aui::lambda_overloaded {
[](const AOptional<system_clock::time_point>& v) -> AString {
if (!v) {
return "";
}
return formatDate(*v);
examples/7guis/timer/src/main.cpp
7GUIs Timer - Timer example.
}),
Horizontal {
Label { "Duration:" },
_new<ASlider>() AUI_LET {
it&& mDuration.biProjected(aui::lambda_overloaded {
[](high_resolution_clock::duration d) -> aui::float_within_0_1 {
return float(d.count()) / float(MAX_DURATION.count());
},
[](aui::float_within_0_1 d) -> high_resolution_clock::duration {
return high_resolution_clock::duration(long(float(d) * float(MAX_DURATION.count())));
notify#
Notify observers that a change was occurred (no preconditions).
In common, you won't need to use this function. AProperty is reevaluated automatically as soon as one updates the value within property.
If your scenario goes beyond AProperty::writeScope()
that explicitly defines modification scope within RAII
scope, you can modify the underlying value by accessing AProperty::raw
and then call AProperty::notify()
to
notify the observers value is changed.
Examples#
examples/7guis/circle_drawer/src/main.cpp
7GUIs Circle Drawer - Undo, redo, dialog control.
readProjected#
Makes a readonly projection of this property.
Examples#
examples/7guis/counter/src/main.cpp
7GUIs Counter - Simple counter.
CounterWindow(): AWindow("AUI - 7GUIs - Counter", 200_dp, 100_dp) {
setContents(Centered {
Horizontal {
_new<ATextField>() AUI_LET {
AObject::connect(mCounter.readProjected(AString::number<int>), it->text());
it->setEditable(false);
},
Button { "Count" }.connect(&AView::clicked, [&] { mCounter += 1; }),
},
});
examples/ui/contacts/src/view/ContactDetailsView.cpp
AUI Contacts - Usage of AUI_DECLARATIVE_FOR to make a contacts-like application.
namespace {
_<AView> profilePhoto(const _<Contact>& contact) {
return Centered {
Label {} & contact->displayName.readProjected([](const AString& s) {
return s.empty() ? "?" : AString(1, s.first()).uppercase();
}) AUI_WITH_STYLE { Opacity(0.5f), FontSize { 32_dp } },
} AUI_WITH_STYLE {
FixedSize { 64_dp },
BorderRadius { 32_dp },
examples/7guis/flight_booker/src/main.cpp
7GUIs Flight Booker - Flight Booker.
} });
setContents(Centered {
Vertical {
_new<ADropdownList>(AListModel<AString>::make({ "one-way flight", "return flight" })) AUI_LET {
connect(it->selectionId().readProjected([](int selectionId) { return selectionId == 1; }),
mIsReturnFlight);
},
dateTextField(mDepartureDate),
dateTextField(mReturnDate) AUI_LET { connect(mIsReturnFlight, AUI_SLOT(it)::setEnabled); },
_new<AButton>("Book") AUI_LET {
examples/7guis/circle_drawer/src/main.cpp
7GUIs Circle Drawer - Undo, redo, dialog control.
Centered {
Horizontal {
Button { "Undo" } AUI_LET {
connect(it->clicked, me::undo);
it & mState.history.nextAction.readProjected([&](UndoStack::Iterator i) { return i != mState.history.begin(); }) > &AView::setEnabled;
},
Button { "Redo" } AUI_LET {
connect(it->clicked, me::redo);
it & mState.history.nextAction.readProjected([&](UndoStack::Iterator i) { return i != mState.history.end(); }) > &AView::setEnabled;
},
examples/7guis/timer/src/main.cpp
7GUIs Timer - Timer example.
writeScope#
- Returns
- aui::PropertyModifier of this property.
Examples#
examples/7guis/circle_drawer/src/main.cpp
7GUIs Circle Drawer - Undo, redo, dialog control.
public:
ActionAddCircle(_<State> state, Circle circle) : mState(std::move(state)), mCircle(std::move(circle)) {}
~ActionAddCircle() override = default;
void undo() override {
mState->circles.writeScope()->pop_back();
}
void redo() override {
mState->circles.writeScope()->push_back(mCircle);
}