From 6ac06f58ef0e464643b6f94c15bcc871c89417f0 Mon Sep 17 00:00:00 2001 From: Adam Fox Date: Mon, 27 Nov 2023 13:15:29 -0800 Subject: [PATCH] APL-CORE: November 2023 Release of APL 2023.3 compilant core engine (2023.3.0) For more details on this release refer to CHANGELOG.md To learn about APL see: https://developer.amazon.com/docs/alexa-presentation-language/understand-apl.html --- CHANGELOG.md | 19 + aplcore/include/apl/action/arrayaction.h | 25 +- aplcore/include/apl/action/autopageaction.h | 6 +- aplcore/include/apl/action/scrollaction.h | 8 +- aplcore/include/apl/action/scrolltoaction.h | 4 +- aplcore/include/apl/action/setpageaction.h | 4 +- aplcore/include/apl/command/arraycommand.h | 2 +- aplcore/include/apl/command/commandfactory.h | 5 + .../include/apl/command/commandproperties.h | 14 + aplcore/include/apl/command/corecommand.h | 1 + .../include/apl/command/insertitemcommand.h | 3 + aplcore/include/apl/command/logcommand.h | 41 + aplcore/include/apl/common.h | 4 +- .../apl/component/actionablecomponent.h | 3 +- .../component/componenteventsourcewrapper.h | 4 +- .../component/componenteventtargetwrapper.h | 4 +- .../apl/component/componenteventwrapper.h | 4 +- .../apl/component/componentproperties.h | 10 + aplcore/include/apl/component/corecomponent.h | 78 +- .../include/apl/component/framecomponent.h | 4 +- aplcore/include/apl/component/hostcomponent.h | 33 +- .../include/apl/component/pagercomponent.h | 15 +- .../apl/component/scrollablecomponent.h | 5 +- .../apl/component/touchablecomponent.h | 1 + .../apl/component/touchwrappercomponent.h | 2 +- .../apl/component/vectorgraphiccomponent.h | 10 + .../include/apl/component/videocomponent.h | 2 +- aplcore/include/apl/content/aplversion.h | 8 +- .../include/apl/content/configurationchange.h | 82 +- aplcore/include/apl/content/content.h | 71 +- aplcore/include/apl/content/documentconfig.h | 21 + aplcore/include/apl/content/importref.h | 20 +- aplcore/include/apl/content/importrequest.h | 35 +- aplcore/include/apl/content/metrics.h | 124 +- aplcore/include/apl/content/package.h | 1 - aplcore/include/apl/content/rootconfig.h | 127 +- aplcore/include/apl/content/rootproperties.h | 9 + aplcore/include/apl/content/settings.h | 2 +- .../apl/datasource/datasourceconnection.h | 2 +- .../offsetindexdatasourceconnection.h | 2 +- .../apl/document/coredocumentcontext.h | 55 +- .../apl/document/documentcontextdata.h | 2 +- aplcore/include/apl/embed/embedrequest.h | 10 +- aplcore/include/apl/engine/builder.h | 2 + aplcore/include/apl/engine/context.h | 4 +- aplcore/include/apl/engine/corerootcontext.h | 19 +- aplcore/include/apl/engine/event.h | 10 +- aplcore/include/apl/engine/layoutmanager.h | 19 +- aplcore/include/apl/engine/propdef.h | 2 + .../include/apl/engine/recalculatesource.h | 4 +- .../include/apl/engine/recalculatetarget.h | 4 +- aplcore/include/apl/engine/rootcontext.h | 5 + .../include/apl/engine/sharedcontextdata.h | 5 +- .../include/apl/extension/extensionclient.h | 2 +- .../apl/extension/extensioncomponent.h | 2 +- .../include/apl/extension/extensionmediator.h | 27 +- aplcore/include/apl/focus/focusmanager.h | 9 +- aplcore/include/apl/graphic/graphic.h | 8 +- aplcore/include/apl/media/coremediamanager.h | 2 +- aplcore/include/apl/media/mediatrack.h | 7 + .../apl/primitives/accessibilityaction.h | 21 +- .../apl/primitives/textmeasurerequest.h | 8 +- aplcore/include/apl/primitives/unicode.h | 8 + aplcore/include/apl/scenegraph/filter.h | 10 +- aplcore/include/apl/scenegraph/layer.h | 1 + aplcore/include/apl/scenegraph/scenegraph.h | 5 + aplcore/include/apl/touch/gesture.h | 5 + .../apl/touch/gestures/doublepressgesture.h | 1 + .../apl/touch/gestures/longpressgesture.h | 1 + .../apl/touch/gestures/swipeawaygesture.h | 5 +- .../include/apl/touch/gestures/tapgesture.h | 1 + aplcore/include/apl/utils/session.h | 24 + aplcore/src/action/animatedscrollaction.cpp | 4 +- aplcore/src/action/arrayaction.cpp | 21 +- aplcore/src/action/autopageaction.cpp | 14 +- aplcore/src/action/scrollaction.cpp | 5 +- aplcore/src/action/scrolltoaction.cpp | 11 +- aplcore/src/action/sequentialaction.cpp | 44 +- aplcore/src/action/setpageaction.cpp | 14 +- aplcore/src/action/speakitemaction.cpp | 24 +- aplcore/src/command/CMakeLists.txt | 1 + aplcore/src/command/autopagecommand.cpp | 7 +- aplcore/src/command/commandfactory.cpp | 8 + aplcore/src/command/commandproperties.cpp | 15 +- aplcore/src/command/corecommand.cpp | 12 +- aplcore/src/command/insertitemcommand.cpp | 34 +- aplcore/src/command/logcommand.cpp | 107 + aplcore/src/command/openurlcommand.cpp | 2 +- aplcore/src/command/parallelcommand.cpp | 53 +- aplcore/src/command/reinflatecommand.cpp | 3 + aplcore/src/command/scrollcommand.cpp | 10 +- .../src/command/scrolltocomponentcommand.cpp | 11 +- aplcore/src/command/scrolltoindexcommand.cpp | 13 +- aplcore/src/command/sequentialcommand.cpp | 1 + aplcore/src/command/setpagecommand.cpp | 7 +- aplcore/src/component/actionablecomponent.cpp | 14 +- .../component/componenteventsourcewrapper.cpp | 2 +- .../component/componenteventtargetwrapper.cpp | 2 +- .../src/component/componenteventwrapper.cpp | 2 +- aplcore/src/component/componentproperties.cpp | 374 +-- aplcore/src/component/corecomponent.cpp | 474 +++- aplcore/src/component/edittextcomponent.cpp | 3 +- aplcore/src/component/framecomponent.cpp | 49 +- aplcore/src/component/hostcomponent.cpp | 318 ++- aplcore/src/component/imagecomponent.cpp | 2 + .../multichildscrollablecomponent.cpp | 21 +- aplcore/src/component/pagercomponent.cpp | 118 +- aplcore/src/component/scrollablecomponent.cpp | 82 +- aplcore/src/component/scrollviewcomponent.cpp | 4 +- aplcore/src/component/textcomponent.cpp | 2 +- aplcore/src/component/touchablecomponent.cpp | 14 +- .../src/component/touchwrappercomponent.cpp | 2 +- .../src/component/vectorgraphiccomponent.cpp | 7 +- aplcore/src/component/videocomponent.cpp | 17 +- aplcore/src/content/aplversion.cpp | 1 + aplcore/src/content/configurationchange.cpp | 55 +- aplcore/src/content/content.cpp | 330 ++- aplcore/src/content/importrequest.cpp | 97 +- aplcore/src/content/metrics.cpp | 8 +- aplcore/src/content/rootconfig.cpp | 1 + aplcore/src/content/rootproperties.cpp | 8 +- aplcore/src/content/viewport.cpp | 4 + .../offsetindexdatasourceconnection.cpp | 2 +- aplcore/src/document/coredocumentcontext.cpp | 145 +- aplcore/src/embed/embedrequest.cpp | 9 +- aplcore/src/engine/builder.cpp | 18 +- aplcore/src/engine/context.cpp | 32 +- aplcore/src/engine/corerootcontext.cpp | 70 +- aplcore/src/engine/layoutmanager.cpp | 169 +- aplcore/src/engine/sharedcontextdata.cpp | 12 +- aplcore/src/engine/tickscheduler.cpp | 2 +- aplcore/src/extension/extensionclient.cpp | 2 +- aplcore/src/extension/extensioncomponent.cpp | 4 +- aplcore/src/extension/extensionmediator.cpp | 28 +- aplcore/src/focus/focusmanager.cpp | 65 +- aplcore/src/graphic/graphic.cpp | 6 +- aplcore/src/graphic/graphicelementtext.cpp | 2 +- aplcore/src/media/mediatrack.cpp | 53 + .../src/primitives/accessibilityaction.cpp | 25 +- aplcore/src/primitives/functions.cpp | 58 +- aplcore/src/primitives/unicode.cpp | 19 + aplcore/src/scenegraph/layer.cpp | 1 + aplcore/src/scenegraph/utilities.cpp | 2 +- aplcore/src/time/sequencer.cpp | 2 +- aplcore/src/touch/gesture.cpp | 19 +- .../src/touch/gestures/doublepressgesture.cpp | 37 +- aplcore/src/touch/gestures/flinggesture.cpp | 5 +- .../src/touch/gestures/longpressgesture.cpp | 21 +- .../src/touch/gestures/pagerflinggesture.cpp | 8 +- aplcore/src/touch/gestures/scrollgesture.cpp | 9 +- .../src/touch/gestures/swipeawaygesture.cpp | 33 +- aplcore/src/touch/gestures/tapgesture.cpp | 23 +- .../utils/unidirectionaleasingscroller.cpp | 3 +- aplcore/src/touch/utils/velocitytracker.cpp | 3 +- aplcore/unit/audio/testaudioplayer.cpp | 19 +- aplcore/unit/audio/testaudioplayerfactory.h | 1 + .../unit/audio/unittest_speak_item_audio.cpp | 140 + .../unit/audio/unittest_speak_list_audio.cpp | 388 +++ aplcore/unit/command/CMakeLists.txt | 5 +- .../command/unittest_command_insertitem.cpp | 109 +- aplcore/unit/command/unittest_command_log.cpp | 143 + .../unit/command/unittest_command_page.cpp | 205 ++ aplcore/unit/command/unittest_commands.cpp | 485 +--- .../command/unittest_commands_parallel.cpp | 195 ++ .../command/unittest_commands_sequential.cpp | 401 +++ aplcore/unit/component/CMakeLists.txt | 1 + .../unittest_accessibility_actions.cpp | 1168 +++++++- .../component/unittest_component_events.cpp | 83 + .../unittest_edit_text_component.cpp | 62 + .../component/unittest_frame_component.cpp | 307 +++ .../component/unittest_host_component.cpp | 545 +++- aplcore/unit/component/unittest_scroll.cpp | 2320 ++++++++-------- aplcore/unit/component/unittest_serialize.cpp | 42 +- aplcore/unit/component/unittest_tick.cpp | 2 +- .../component/unittest_video_component.cpp | 213 ++ aplcore/unit/content/CMakeLists.txt | 1 + aplcore/unit/content/unittest_document.cpp | 70 +- .../content/unittest_document_background.cpp | 13 + aplcore/unit/content/unittest_metrics.cpp | 7 +- aplcore/unit/content/unittest_packages.cpp | 2411 +++++++++++++++++ aplcore/unit/content/unittest_rootconfig.cpp | 11 + aplcore/unit/datagrammar/unittest_grammar.cpp | 37 +- .../unittest_dynamicindexlistupdate.cpp | 6 +- .../unit/embed/unittest_documentcreate.cpp | 45 + .../embed/unittest_embedded_extensions.cpp | 2 +- .../embed/unittest_embedded_lifecycle.cpp | 725 ++++- .../embed/unittest_embedded_reinflate.cpp | 171 +- aplcore/unit/engine/unittest_builder.cpp | 5 +- .../unit/engine/unittest_builder_pager.cpp | 2 +- .../unit/engine/unittest_builder_preserve.cpp | 6 +- .../unit/engine/unittest_builder_sequence.cpp | 56 +- aplcore/unit/engine/unittest_context.cpp | 48 +- .../engine/unittest_context_apl_version.cpp | 4 +- aplcore/unit/engine/unittest_layouts.cpp | 40 + .../extension/unittest_extension_client.cpp | 105 +- .../extension/unittest_extension_mediator.cpp | 350 ++- .../unittest_requested_extension.cpp | 43 + aplcore/unit/focus/unittest_focus_manager.cpp | 37 + aplcore/unit/graphic/unittest_graphic.cpp | 3 +- .../graphic/unittest_graphic_component.cpp | 93 +- .../livedata/unittest_livearray_rebuild.cpp | 2 +- aplcore/unit/media/unittest_media_player.cpp | 39 +- aplcore/unit/primitives/unittest_unicode.cpp | 28 + aplcore/unit/scaling/unittest_auto_size.cpp | 673 ++++- aplcore/unit/scenegraph/CMakeLists.txt | 2 + aplcore/unit/scenegraph/test_sg.cpp | 1 + aplcore/unit/scenegraph/test_sg.h | 3 + .../scenegraph/unittest_sg_accessibility.cpp | 111 +- aplcore/unit/scenegraph/unittest_sg_base.cpp | 81 + aplcore/unit/scenegraph/unittest_sg_frame.cpp | 84 + .../unit/scenegraph/unittest_sg_graphic.cpp | 76 +- .../unittest_sg_graphic_component.cpp | 27 +- .../unittest_sg_graphic_loading.cpp | 18 +- aplcore/unit/scenegraph/unittest_sg_image.cpp | 12 + .../unittest_sg_line_highlighting.cpp | 126 +- aplcore/unit/scenegraph/unittest_sg_pager.cpp | 122 +- .../scenegraph/unittest_sg_pathparser.cpp | 2 + aplcore/unit/scenegraph/unittest_sg_text.cpp | 9 +- aplcore/unit/scenegraph/unittest_sg_touch.cpp | 4 +- aplcore/unit/scenegraph/unittest_sg_video.cpp | 63 + aplcore/unit/test_comparisons.h | 10 + aplcore/unit/testeventloop.cpp | 57 +- aplcore/unit/testeventloop.h | 40 +- .../unittest_native_gestures_scrollable.cpp | 4 +- .../unit/unittest_simpletextmeasurement.cpp | 2 +- aplcore/unit/utils/unittest_path.cpp | 1 + aplcore/unit/utils/unittest_session.cpp | 20 +- doc/core_objects.puml | 486 +++- extensions/alexaext/CMakeLists.txt | 1 + .../AplAudioNormalizationExtension.h | 105 + .../alexaext/include/alexaext/alexaext.h | 1 + .../AplAudioNormalizationExtension.cpp | 121 + .../AplMetricsExtension.cpp | 4 +- extensions/unit/CMakeLists.txt | 3 +- .../unit/unittest_apl_audio_normalization.cpp | 216 ++ extensions/unit/unittest_apl_metric.cpp | 30 +- test/parseDirective.cpp | 2 +- test/utils.h | 2 +- 238 files changed, 15149 insertions(+), 2986 deletions(-) create mode 100644 aplcore/include/apl/command/logcommand.h create mode 100644 aplcore/src/command/logcommand.cpp create mode 100644 aplcore/unit/command/unittest_command_log.cpp create mode 100644 aplcore/unit/command/unittest_commands_parallel.cpp create mode 100644 aplcore/unit/command/unittest_commands_sequential.cpp create mode 100644 aplcore/unit/component/unittest_video_component.cpp create mode 100644 aplcore/unit/content/unittest_packages.cpp create mode 100644 aplcore/unit/scenegraph/unittest_sg_base.cpp create mode 100644 aplcore/unit/scenegraph/unittest_sg_video.cpp create mode 100644 extensions/alexaext/include/alexaext/APLAudioNormalizationExtension/AplAudioNormalizationExtension.h create mode 100644 extensions/alexaext/src/APLAudioNormalizationExtension/AplAudioNormalizationExtension.cpp create mode 100644 extensions/unit/unittest_apl_audio_normalization.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index c7d31f0..b629372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [2023.3] + +This release adds support for version 2023.3 of the APL specification. + +### Added + +- Add Log command +- Support conditional imports +- Support gradient as Frame background +- Add duration control for scrolling animations +- Add onChildrenChanged handler to Multichild component +- Add data field expansion for Sequential and Parallel commands +- Alpha feature: Support autosizing of Host component + +### Changed + +- Bug fixes +- Performance improvements + ## [2023.2] This release adds support for version 2023.2 of the APL specification. diff --git a/aplcore/include/apl/action/arrayaction.h b/aplcore/include/apl/action/arrayaction.h index a61c7f4..0d14e46 100644 --- a/aplcore/include/apl/action/arrayaction.h +++ b/aplcore/include/apl/action/arrayaction.h @@ -25,21 +25,38 @@ namespace apl { class ArrayAction : public Action { public: static std::shared_ptr make(const TimersPtr& timers, - std::shared_ptr&& command, + const ContextPtr& context, + std::shared_ptr&& command, + CommandData&& data, bool fastMode) { - auto ptr = std::make_shared(timers, std::move(command), fastMode); + auto ptr = std::make_shared( + timers, context, std::move(command), std::move(data), fastMode); ptr->advance(); return ptr; } - ArrayAction(const TimersPtr& timers, std::shared_ptr&& command, bool fastMode); + static std::shared_ptr make(const TimersPtr& timers, + std::shared_ptr&& command, + bool fastMode) { + return make(timers, command->context(), std::move(command), + CommandData(command->data()), fastMode); + } + + ArrayAction( + const TimersPtr& timers, + const ContextPtr& context, + std::shared_ptr&& command, + CommandData&& data, + bool fastMode); private: void advance(); private: - const std::shared_ptr mCommand; + const std::shared_ptr mCommand; const bool mFastMode; + ContextPtr mContext; + CommandData mData; size_t mNextIndex; diff --git a/aplcore/include/apl/action/autopageaction.h b/aplcore/include/apl/action/autopageaction.h index 9abbeee..5370968 100644 --- a/aplcore/include/apl/action/autopageaction.h +++ b/aplcore/include/apl/action/autopageaction.h @@ -37,7 +37,8 @@ class AutoPageAction : public ResourceHoldingAction { const ComponentPtr& container, int start, int end, - apl_time_t duration); + apl_duration_t duration, + apl_duration_t transitionDuration); void freeze() override; bool rehydrate(const CoreDocumentContext& context) override; @@ -52,7 +53,8 @@ class AutoPageAction : public ResourceHoldingAction { size_t mCurrentIndex; size_t mNextIndex; size_t mEndIndex; - apl_time_t mDuration; + apl_duration_t mDuration; + apl_duration_t mTransitionDuration; }; } // namespace apl diff --git a/aplcore/include/apl/action/scrollaction.h b/aplcore/include/apl/action/scrollaction.h index c8d963b..489b62a 100644 --- a/aplcore/include/apl/action/scrollaction.h +++ b/aplcore/include/apl/action/scrollaction.h @@ -32,24 +32,26 @@ class ScrollAction : public AnimatedScrollAction { /** * @param timers Timer reference. * @param command Command that spawned this action. + * @param duration scrolling duration, -1 to use runtime-default. * @return A scroll action or null if not needed. */ static std::shared_ptr make(const TimersPtr& timers, - const std::shared_ptr& command); + const std::shared_ptr& command, + apl_duration_t duration); /** * @param timers Timer reference. * @param context context ot run this action in. * @param target component to perform action on. * @param targetDistance Object containing Dimension representing distance to be scrolled. - * @param duration scrolling duration. + * @param duration scrolling duration, -1 to use runtime-default. * @return The scroll action or null if it is not needed. */ static std::shared_ptr make(const TimersPtr& timers, const ContextPtr& context, const CoreComponentPtr& target, const Object& targetDistance, - apl_duration_t duration = -1); + apl_duration_t duration); ScrollAction(const TimersPtr& timers, const ContextPtr& context, diff --git a/aplcore/include/apl/action/scrolltoaction.h b/aplcore/include/apl/action/scrolltoaction.h index c6b0be7..634740e 100644 --- a/aplcore/include/apl/action/scrolltoaction.h +++ b/aplcore/include/apl/action/scrolltoaction.h @@ -34,11 +34,13 @@ class ScrollToAction : public AnimatedScrollAction { * @param timers Timer reference. * @param command Command that spawned this action. * @param target Component to scroll to. + * @param duration Scrolling duration. * @return The scroll to action or null if it is not needed. */ static std::shared_ptr make(const TimersPtr& timers, const std::shared_ptr& command, - const CoreComponentPtr& target = nullptr); + const CoreComponentPtr& target = nullptr, + apl_duration_t duration = -1); /** * Called from SpeakItem during line highlight mode. * @param timers Timer reference. diff --git a/aplcore/include/apl/action/setpageaction.h b/aplcore/include/apl/action/setpageaction.h index f5d9baf..bc3238d 100644 --- a/aplcore/include/apl/action/setpageaction.h +++ b/aplcore/include/apl/action/setpageaction.h @@ -33,7 +33,8 @@ class SetPageAction : public ResourceHoldingAction { SetPageAction(const TimersPtr& timers, const std::shared_ptr& command, - const CoreComponentPtr& target); + const CoreComponentPtr& target, + apl_duration_t transitionDuration); void freeze() override; bool rehydrate(const CoreDocumentContext& context) override; @@ -45,6 +46,7 @@ class SetPageAction : public ResourceHoldingAction { std::shared_ptr mCommand; CoreComponentPtr mTarget; int mTargetIndex; + apl_duration_t mTransitionDuration; }; } // namespace apl diff --git a/aplcore/include/apl/command/arraycommand.h b/aplcore/include/apl/command/arraycommand.h index cc9e6e7..891e801 100644 --- a/aplcore/include/apl/command/arraycommand.h +++ b/aplcore/include/apl/command/arraycommand.h @@ -54,7 +54,7 @@ class ArrayCommand : public CoreCommand { std::string name() const override { return "ArrayCommand"; } ActionPtr execute(const TimersPtr& timers, bool fastMode) override; - bool finishAllOnTerminate() const { return mFinishAllOnTerminate; } + bool finishAllOnTerminate() const override { return mFinishAllOnTerminate; } private: bool mFinishAllOnTerminate; diff --git a/aplcore/include/apl/command/commandfactory.h b/aplcore/include/apl/command/commandfactory.h index 5bba881..90e55e9 100644 --- a/aplcore/include/apl/command/commandfactory.h +++ b/aplcore/include/apl/command/commandfactory.h @@ -49,6 +49,11 @@ class CommandFactory : public NonCopyable { CommandPtr inflate(const ContextPtr& context, CommandData&& commandData, const CoreComponentPtr& base); CommandPtr inflate(CommandData&& commandData, const std::shared_ptr& parent); + + CommandPtr inflate( + const ContextPtr& context, + CommandData&& commandData, + const std::shared_ptr& parent); protected: CommandFactory() = default;; diff --git a/aplcore/include/apl/command/commandproperties.h b/aplcore/include/apl/command/commandproperties.h index 2a8698c..f479778 100644 --- a/aplcore/include/apl/command/commandproperties.h +++ b/aplcore/include/apl/command/commandproperties.h @@ -48,6 +48,7 @@ enum CommandType { kCommandTypeCustomEvent, kCommandTypeInsertItem, kCommandTypeRemoveItem, + kCommandTypeLog, }; enum CommandScrollAlign { @@ -94,6 +95,14 @@ enum CommandReason { kCommandReasonExit }; +enum CommandLogLevel { + kCommandLogLevelDebug, + kCommandLogLevelInfo, + kCommandLogLevelWarn, + kCommandLogLevelError, + kCommandLogLevelCritical +}; + enum CommandPropertyKey { kCommandPropertyAlign, kCommandPropertyArguments, @@ -116,6 +125,8 @@ enum CommandPropertyKey { kCommandPropertyHighlightMode, kCommandPropertyIndex, kCommandPropertyItem, + kCommandPropertyLevel, + kCommandPropertyMessage, kCommandPropertyMinimumDwellTime, kCommandPropertyOnFail, kCommandPropertyOtherwise, @@ -130,6 +141,8 @@ enum CommandPropertyKey { kCommandPropertySource, kCommandPropertyStart, kCommandPropertyState, + kCommandPropertyTargetDuration, + kCommandPropertyTransitionDuration, kCommandPropertyValue, }; @@ -142,6 +155,7 @@ extern Bimap sCommandAudioTrackMap; extern Bimap sControlMediaMap; extern Bimap sCommandRepeatModeMap; extern Bimap sCommandReasonMap; +extern Bimap sCommandLogLevelMap; using CommandBag = ObjectBag; diff --git a/aplcore/include/apl/command/corecommand.h b/aplcore/include/apl/command/corecommand.h index ff43871..f6d91b6 100644 --- a/aplcore/include/apl/command/corecommand.h +++ b/aplcore/include/apl/command/corecommand.h @@ -96,6 +96,7 @@ class CoreCommand : public Command { const Properties& properties() const { return mProperties; } virtual const CommandPropDefSet& propDefSet() const; + virtual bool finishAllOnTerminate() const { return false; } void freeze() final; bool rehydrate(const CoreDocumentContext& context) final; diff --git a/aplcore/include/apl/command/insertitemcommand.h b/aplcore/include/apl/command/insertitemcommand.h index 748ea18..bef970a 100644 --- a/aplcore/include/apl/command/insertitemcommand.h +++ b/aplcore/include/apl/command/insertitemcommand.h @@ -29,6 +29,9 @@ class InsertItemCommand : public TemplatedCommand { CommandType type() const override { return kCommandTypeInsertItem; } ActionPtr execute(const TimersPtr& timers, bool fastMode) override; + +private: + ContextPtr buildBaseChildContext(int insertIndex) const; }; } // namespace apl diff --git a/aplcore/include/apl/command/logcommand.h b/aplcore/include/apl/command/logcommand.h new file mode 100644 index 0000000..8db4903 --- /dev/null +++ b/aplcore/include/apl/command/logcommand.h @@ -0,0 +1,41 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef _APL_LOG_COMMAND_H +#define _APL_LOG_COMMAND_H + +#include "apl/command/corecommand.h" + +namespace apl { + +class LogCommand : public TemplatedCommand { +public: + COMMAND_CONSTRUCTOR(LogCommand); + + const CommandPropDefSet& propDefSet() const override; + + CommandType type() const override { return kCommandTypeLog; } + + ActionPtr execute(const TimersPtr& timers, bool fastMode) override; + +private: + rapidjson::Document mDocument; + rapidjson::Value mSource; + rapidjson::Value mArguments; +}; + +} // namespace apl + +#endif // _APL_LOG_COMMAND_H diff --git a/aplcore/include/apl/common.h b/aplcore/include/apl/common.h index 02bc95a..a7c4eb3 100644 --- a/aplcore/include/apl/common.h +++ b/aplcore/include/apl/common.h @@ -48,6 +48,7 @@ using apl_duration_t = double; // Common definitions of shared pointer data structures. We define the XXXPtr variations // here so they can be conveniently used from any source file. +class AccessibilityAction; class Action; class AudioPlayer; class AudioPlayerFactory; @@ -96,6 +97,7 @@ class TextMeasurement; class Timers; class UIDObject; +using AccessibilityActionPtr = std::shared_ptr; using ActionPtr = std::shared_ptr; using AudioPlayerFactoryPtr = std::shared_ptr; using AudioPlayerPtr = std::shared_ptr; @@ -107,10 +109,10 @@ using ContentPtr = std::shared_ptr; using ContextPtr = std::shared_ptr; using ContextDataPtr = std::shared_ptr; using CoreComponentPtr = std::shared_ptr; +using ConstCoreComponentPtr = std::shared_ptr; using CoreDocumentContextPtr = std::shared_ptr; using CoreRootContextPtr = std::shared_ptr; using DataSourceProviderPtr = std::shared_ptr; -using DataSourcePtr = std::shared_ptr; using DependantPtr = std::shared_ptr; using DocumentConfigPtr = std::shared_ptr; using DocumentContextDataPtr = std::shared_ptr; diff --git a/aplcore/include/apl/component/actionablecomponent.h b/aplcore/include/apl/component/actionablecomponent.h index 83bef26..ddc434b 100644 --- a/aplcore/include/apl/component/actionablecomponent.h +++ b/aplcore/include/apl/component/actionablecomponent.h @@ -60,7 +60,7 @@ class ActionableComponent : public CoreComponent { * @param component Pointer to cast. * @return Casted pointer to this type, nullptr if not possible. */ - static std::shared_ptr cast(const std::shared_ptr& component); + static std::shared_ptr cast(const ComponentPtr& component); /// CoreComponent overrides bool isActionable() const override { return true; } @@ -81,6 +81,7 @@ class ActionableComponent : public CoreComponent { CoreComponent(context, std::move(properties), path), mGesturesDisabled(false) {} bool processGestures(const PointerEvent& event, apl_time_t timestamp) override; + void getSupportedStandardAccessibilityActions(std::map& result) const override; void invokeStandardAccessibilityAction(const std::string& name) override; #ifdef SCENEGRAPH diff --git a/aplcore/include/apl/component/componenteventsourcewrapper.h b/aplcore/include/apl/component/componenteventsourcewrapper.h index e2f499c..cca6edb 100644 --- a/aplcore/include/apl/component/componenteventsourcewrapper.h +++ b/aplcore/include/apl/component/componenteventsourcewrapper.h @@ -29,11 +29,11 @@ namespace apl { */ class ComponentEventSourceWrapper : public ComponentEventWrapper { public: - static std::shared_ptr create(const std::shared_ptr& component, + static std::shared_ptr create(const ConstCoreComponentPtr& component, std::string handler, const Object& value); - explicit ComponentEventSourceWrapper(const std::shared_ptr& component) + explicit ComponentEventSourceWrapper(const ConstCoreComponentPtr& component) : ComponentEventWrapper(component) {} std::string toDebugString() const override { return "ComponentEventSourceWrapper<>"; } diff --git a/aplcore/include/apl/component/componenteventtargetwrapper.h b/aplcore/include/apl/component/componenteventtargetwrapper.h index ec99d1a..6ac455d 100644 --- a/aplcore/include/apl/component/componenteventtargetwrapper.h +++ b/aplcore/include/apl/component/componenteventtargetwrapper.h @@ -29,9 +29,9 @@ namespace apl { */ class ComponentEventTargetWrapper : public ComponentEventWrapper { public: - static std::shared_ptr create(const std::shared_ptr& component); + static std::shared_ptr create(const ConstCoreComponentPtr& component); - explicit ComponentEventTargetWrapper(const std::shared_ptr& component) + explicit ComponentEventTargetWrapper(const ConstCoreComponentPtr& component) : ComponentEventWrapper(component) {} std::string toDebugString() const override { return "ComponentEventTargetWrapper<>"; } diff --git a/aplcore/include/apl/component/componenteventwrapper.h b/aplcore/include/apl/component/componenteventwrapper.h index a7a751e..b81482e 100644 --- a/aplcore/include/apl/component/componenteventwrapper.h +++ b/aplcore/include/apl/component/componenteventwrapper.h @@ -33,7 +33,7 @@ class ComponentEventTargetWrapper; */ class ComponentEventWrapper : public ObjectData { public: - explicit ComponentEventWrapper(const std::shared_ptr& component); + explicit ComponentEventWrapper(const ConstCoreComponentPtr& component); Object get(const std::string& key) const override; Object opt(const std::string& key, const Object& def) const override; @@ -42,7 +42,7 @@ class ComponentEventWrapper : public ObjectData { const ObjectMap& getMap() const override; - std::shared_ptr getComponent() const { return mComponent.lock(); } + ConstCoreComponentPtr getComponent() const { return mComponent.lock(); } virtual bool operator==(const ComponentEventWrapper& rhs) const = 0; virtual bool operator==(const ComponentEventSourceWrapper& rhs) const { return false; } diff --git a/aplcore/include/apl/component/componentproperties.h b/aplcore/include/apl/component/componentproperties.h index 8752016..49ba992 100644 --- a/aplcore/include/apl/component/componentproperties.h +++ b/aplcore/include/apl/component/componentproperties.h @@ -363,6 +363,8 @@ enum PropertyKey { kPropertyScrollDirection, /// An array of accessibility actions associated with this component kPropertyAccessibilityActions, + /// An array of assigned accessibility actions + kPropertyAccessibilityActionsAssigned, /// Component accessibility label kPropertyAccessibilityLabel, /// ImageComponent and VectorGraphicComponent alignment (see #ImageAlign, #VectorGraphicAlign) @@ -379,6 +381,10 @@ enum PropertyKey { kPropertyMuted, /// FrameComponent background color kPropertyBackgroundColor, + /// FrameComponent background assigned + kPropertyBackgroundAssigned, + /// FrameComponent background + kPropertyBackground, /// FrameComponent border bottom-left radius (input only) kPropertyBorderBottomLeftRadius, /// FrameComponent border bottom-right radius (input only) @@ -541,6 +547,8 @@ enum PropertyKey { kPropertyOnBlur, /// TouchableComponent handler for cancel kPropertyOnCancel, + /// Multi-child component children changed + kPropertyOnChildrenChanged, /// TouchableComponent handler for down kPropertyOnDown, /// VideoComponent handler for video end @@ -609,6 +617,8 @@ enum PropertyKey { kPropertyPageId, /// Pager virtual property for the index of the current page kPropertyPageIndex, + /// Explicit parameter-passing property map for HostComponent and VectorGraphicComponent + kPropertyParameters, /// VideoComponent current playing state kPropertyPlayingState, /// ContainerComponent child absolute or relative position (see #Position) diff --git a/aplcore/include/apl/component/corecomponent.h b/aplcore/include/apl/component/corecomponent.h index c6bdf78..66bc9ea 100644 --- a/aplcore/include/apl/component/corecomponent.h +++ b/aplcore/include/apl/component/corecomponent.h @@ -272,7 +272,7 @@ class CoreComponent : public Component, * @param key The property or data-binding to retrieve * @return The value or null if the property does not exist */ - Object getProperty(const std::string& key) { return getPropertyAndWriteableState(key).first; } + Object getProperty(const std::string& key) const { return getPropertyAndWriteableState(key).first; } /** * Return the value of a component property. This is the opposite of the @@ -280,7 +280,7 @@ class CoreComponent : public Component, * @param key The property to retrieve * @return The value or null if the property does not exist */ - Object getProperty(PropertyKey key); + Object getProperty(PropertyKey key) const; /** * Mark a property as being changed. This only applies to properties set to @@ -435,14 +435,14 @@ class CoreComponent : public Component, /** * Convert this component and all of its properties into a human-readable JSON object * @param allocator RapidJSON memory allocator - * @return The object + * @return The object. */ rapidjson::Value serializeAll(rapidjson::Document::AllocatorType& allocator) const override; /** * Convert the dirty properties of this component into a JSON object. * @param allocator RapidJSON memory allocator - * @return The obje + * @return The object. */ rapidjson::Value serializeDirty(rapidjson::Document::AllocatorType& allocator) override; @@ -633,6 +633,11 @@ class CoreComponent : public Component, return isFocusable() || !getCalculated(apl::kPropertyAccessibilityLabel).empty(); } + /** + * Refresh accessibility actions set in case if any relevant parameters changed. + */ + void refreshAccessibilityActions(bool useDirtyFlag); + /** * Get visible children of component and respective visibility values. * @param realOpacity cumulative opacity. @@ -658,10 +663,15 @@ class CoreComponent : public Component, /** * @return true when the component has 'normal' display property and an - * opacity greater than zero. + * opacity greater than zero and is not disallowed. */ bool isDisplayable() const; + /** + * @return true when the component has been disallowed by the runtime, false otherwise. + */ + bool isDisallowed() const { return mIsDisallowed; } + /** * Calculate real opacity of component. * @param parentRealOpacity parent component real opacity. @@ -787,7 +797,7 @@ class CoreComponent : public Component, * Call this method to get shared ptr of CoreComponent * @return shared ptr of type CoreComponent. */ - std::shared_ptr shared_from_corecomponent() + CoreComponentPtr shared_from_corecomponent() { return std::static_pointer_cast(shared_from_this()); } @@ -796,7 +806,7 @@ class CoreComponent : public Component, * Call this method to get shared ptr of const CoreComponent * @return shared ptr of type const CoreComponent. */ - std::shared_ptr shared_from_corecomponent() const + ConstCoreComponentPtr shared_from_corecomponent() const { return std::static_pointer_cast(shared_from_this()); } @@ -956,11 +966,21 @@ class CoreComponent : public Component, */ void setStickyOffset(Point stickyOffset) { mStickyOffset = stickyOffset; } + /** + * Perform any operations which is not layout based, but may depend on previous processing. + */ + void postClearPending(); + /** * @param component Pointer to cast. * @return Casted pointer to this type, nullptr if not possible. */ - static std::shared_ptr cast(const std::shared_ptr& component); + static CoreComponentPtr cast(const ComponentPtr& component); + + /** + * Mark this component needing accessibility actions refreshed. + */ + void markAccessibilityDirty(); #ifdef SCENEGRAPH /** @@ -1023,10 +1043,10 @@ class CoreComponent : public Component, virtual void ensureDisplayedChildren(); /** - * @return True if layout change calculations should not be propagated to component's children. Usually the case - * when component itself is not taking part in the layout tree. + * @return True if layout change calculations should be propagated to component's children. Usually the case + * when component itself is part of the layout tree. */ - bool shouldNotPropagateLayoutChanges() const; + bool shouldPropagateLayoutChanges() const; /** * @return hash of properties that could affect TextMeasurement. @@ -1115,13 +1135,24 @@ class CoreComponent : public Component, return { Object::NULL_OBJECT(), false }; } -protected: /** * @return true if children of this component should be included in the visual context, false * otherwise. */ virtual bool includeChildrenInVisualContext() const { return true; } + /** + * @param result Supported standard accessibility actions, paired with true if implicit + * (does not need to be enabled) + */ + virtual void getSupportedStandardAccessibilityActions(std::map& result) const {} + + /** + * @param child Component to check + * @return true if current component is hierarchical parent of provided one, false otherwise. + */ + bool isParentOf(const CoreComponentPtr& child); + private: friend streamer& operator<<(streamer&, const Component&); @@ -1131,6 +1162,11 @@ class CoreComponent : public Component, friend class ChildWalker; friend class HostComponent; // for access to attachedToParent + enum ChildChangeAction { + kChildChangeActionInsert, + kChildChangeActionRemove + }; + bool appendChild(const ComponentPtr& child, bool useDirtyFlag); void attachedToParent(const CoreComponentPtr& parent); @@ -1168,7 +1204,7 @@ class CoreComponent : public Component, std::shared_ptr createEventProperties(const std::string& handler, const Object& value) const; - void notifyChildChanged(size_t index, const std::string& uid, const std::string& action); + void notifyChildChanged(size_t index, const CoreComponentPtr& component, ChildChangeAction action); /** * The default behavior of the child insertion is to attach the child when it happens. @@ -1188,6 +1224,12 @@ class CoreComponent : public Component, YGSize textMeasureInternal(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode); float textBaselineInternal(float width, float height); + void fixAccessibilityActions(); + + void processChildrenChanges(); + + static std::string toStringAction(ChildChangeAction action); + protected: bool mInheritParentState; State mState; // Operating state (pressed, checked, etc) @@ -1202,6 +1244,7 @@ class CoreComponent : public Component, std::shared_ptr mRebuilder; Size mLayoutSize; bool mDisplayedChildrenStale; + bool mIsDisallowed; #ifdef SCENEGRAPH sg::LayerPtr mSceneGraphLayer; #endif // SCENEGRAPH @@ -1209,6 +1252,14 @@ class CoreComponent : public Component, private: // The members below are used to store cached values for performance reasons, and not part of // the state of this component. + struct ChildChange { + CoreComponentPtr component; + ChildChangeAction action; + size_t index; + }; + + std::vector mChildrenChanges; + Transform2D mGlobalToLocal; bool mGlobalToLocalIsStale; Point mStickyOffset; @@ -1216,6 +1267,7 @@ class CoreComponent : public Component, bool mVisualHashStale; std::string mTextMeasurementHash; timeout_id mTickHandlerId = 0; + bool mAccessibilityDirty = false; }; } // namespace apl diff --git a/aplcore/include/apl/component/framecomponent.h b/aplcore/include/apl/component/framecomponent.h index 4b50909..2cf881d 100644 --- a/aplcore/include/apl/component/framecomponent.h +++ b/aplcore/include/apl/component/framecomponent.h @@ -30,7 +30,9 @@ class FrameComponent : public CoreComponent { void setHeight(const Dimension& height) override; void setWidth(const Dimension& width) override; - void fixBorder(bool useDirtyFlag=true); + void fixBorder(bool useDirtyFlag = true); + void fixBackgroundByColor(bool useDirtyFlag = true); + void fixBackground(bool useDirtyFlag = true); protected: const ComponentPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/component/hostcomponent.h b/aplcore/include/apl/component/hostcomponent.h index 1aee548..4b8f83e 100644 --- a/aplcore/include/apl/component/hostcomponent.h +++ b/aplcore/include/apl/component/hostcomponent.h @@ -22,6 +22,7 @@ #include "apl/component/actionablecomponent.h" #include "apl/component/componentpropdef.h" #include "apl/component/componentproperties.h" +#include "apl/content/configurationchange.h" #include "apl/embed/documentmanager.h" #include "apl/engine/properties.h" #include "apl/utils/path.h" @@ -38,6 +39,8 @@ class HostComponent : public ActionableComponent { ComponentType getType() const override { return kComponentTypeHost; }; + bool getTags(rapidjson::Value& outMap, rapidjson::Document::AllocatorType& allocator) override; + ComponentPtr findComponentById(const std::string& id, bool traverseHost) const override; bool singleChild() const override { return true; } @@ -48,9 +51,24 @@ class HostComponent : public ActionableComponent { /** * Reinflate contained document. + * @return Action to be resolved by runtime, if any. */ void reinflate(); + /** + * Embedded-specific processing for Embedded content to "enhance" it with evaluation + * capabilities if required. + * @param content Embedded document content. + * @param documentConfig Document config. + */ + void refreshContent(const ContentPtr& content, const DocumentConfigPtr& documentConfig) const; + + ConfigurationChange filterConfigurationChange( + const ConfigurationChange& configurationChange, + const Metrics& metrics) const; + + static std::shared_ptr cast(const ComponentPtr& component); + protected: const ComponentPropDefSet& propDefSet() const override; @@ -67,7 +85,7 @@ class HostComponent : public ActionableComponent { bool executeKeyHandlers(KeyHandlerType type, const Keyboard &keyboard) override; private: - DocumentContextPtr onLoad(const EmbeddedRequestSuccessResponse&& response); + DocumentContextPtr onLoad(EmbeddedRequestSuccessResponse&& response); void onLoadHandler(); @@ -75,7 +93,7 @@ class HostComponent : public ActionableComponent { void onFailHandler(const URLRequest& url, const std::string& failure); - DocumentContextPtr initializeEmbedded(const EmbeddedRequestSuccessResponse&& response); + DocumentContextPtr initializeEmbedded(EmbeddedRequestSuccessResponse&& response); void detachEmbedded(); @@ -83,7 +101,7 @@ class HostComponent : public ActionableComponent { void requestEmbedded(); - void resolvePendingParameters(const ContentPtr& content); + void resolvePendingParameters(const ContentPtr& content) const; /** * @return Owned document ID, 0 if none. @@ -92,10 +110,19 @@ class HostComponent : public ActionableComponent { void setDocument(int id, bool connectedVC); + RootConfigPtr generateChildConfig(const DocumentConfigPtr& documentConfig) const; + Metrics generateChildMetrics() const; + + void finalizeReinflate(const CoreDocumentContextPtr& document); + + bool isAutoWidth() const; + bool isAutoHeight() const; + private: EmbedRequestPtr mRequest; bool mOnLoadOnFailReported = false; bool mNeedToRequestDocument = true; + std::pair> mReinflationState; }; } // namespace apl diff --git a/aplcore/include/apl/component/pagercomponent.h b/aplcore/include/apl/component/pagercomponent.h index 0cc62ba..adcd22f 100644 --- a/aplcore/include/apl/component/pagercomponent.h +++ b/aplcore/include/apl/component/pagercomponent.h @@ -36,7 +36,7 @@ class PagerComponent : public ActionableComponent { * @param component Pointer to cast. * @return Casted pointer to this type, nullptr if not possible. */ - static std::shared_ptr cast(const std::shared_ptr& component); + static std::shared_ptr cast(const ComponentPtr& component); /// Component overrides. void initialize() override; @@ -60,16 +60,18 @@ class PagerComponent : public ActionableComponent { /** * Command page switch helper function. - * @param context Execution context. * @param target Target component (needs to be pager). * @param index page to switch to. Should be in valid [0; mChildren.size()) range. * @param direction Paging direction. * @param ref Action reference to resolve after switch completed. * @param skipDefaultAnimation if set to true no page switch animation will be performed in * case if no custom processing was assigned with handlePageMove. + * @param transitionDuration transition duration override, instant if 0, default will be used if + * negative. */ - static void setPageUtil(const ContextPtr& context, const ComponentPtr& target, int index, - PageDirection direction, const ActionRef& ref, bool skipDefaultAnimation = false); + static void setPageUtil(const ComponentPtr& target, int index, PageDirection direction, + const ActionRef& ref, bool skipDefaultAnimation = false, + apl_duration_t transitionDuration = -1); /** * Start page move process. Set initial states. Should be followed by executePageMove and endPageMove. @@ -105,6 +107,8 @@ class PagerComponent : public ActionableComponent { void ensureDisplayedChildren() override; void releaseSelf() override; void clearActiveStateSelf() override; + void invokeStandardAccessibilityAction(const std::string& name) override; + void getSupportedStandardAccessibilityActions(std::map& result) const override; private: bool multiChild() const override { return true; } @@ -115,7 +119,8 @@ class PagerComponent : public ActionableComponent { ActionPtr executePageChangeEvent(bool fast); void setPage(int page); void setPageImmediate(int pageIndex); - void handleSetPage(int index, PageDirection direction, const ActionRef& ref, bool skipDefaultAnimation); + void setPageImmediate(PageDirection direction); + void handleSetPage(int index, PageDirection direction, const ActionRef& ref, bool skipDefaultAnimation, apl_duration_t transitionDuration = -1); PageDirection focusDirectionToPage(FocusDirection direction); void reportLoadedInternal(size_t index); diff --git a/aplcore/include/apl/component/scrollablecomponent.h b/aplcore/include/apl/component/scrollablecomponent.h index 5d8aae5..2c4a841 100644 --- a/aplcore/include/apl/component/scrollablecomponent.h +++ b/aplcore/include/apl/component/scrollablecomponent.h @@ -40,7 +40,7 @@ class ScrollableComponent : public ActionableComponent { * @param component Pointer to cast. * @return Casted pointer to this type, nullptr if not possible. */ - static std::shared_ptr cast(const std::shared_ptr& component); + static std::shared_ptr cast(const ComponentPtr& component); void update(UpdateType type, float value) override; bool canConsumeFocusDirectionEvent(FocusDirection direction, bool fromInside) override; @@ -57,6 +57,8 @@ class ScrollableComponent : public ActionableComponent { bool getTags(rapidjson::Value& outMap, rapidjson::Document::AllocatorType& allocator) override; bool scrollable() const override { return true; } const ComponentPropDefSet& propDefSet() const override; + void invokeStandardAccessibilityAction(const std::string& name) override; + void getSupportedStandardAccessibilityActions(std::map& result) const override; /** * Override this to calculate maximum available scroll position. @@ -83,6 +85,7 @@ class ScrollableComponent : public ActionableComponent { #endif // SCENEGRAPH private: + void scroll(bool backwards); bool setScrollPositionInternal(float value); bool canScroll(FocusDirection direction); diff --git a/aplcore/include/apl/component/touchablecomponent.h b/aplcore/include/apl/component/touchablecomponent.h index ea76bf7..7ca437f 100644 --- a/aplcore/include/apl/component/touchablecomponent.h +++ b/aplcore/include/apl/component/touchablecomponent.h @@ -56,6 +56,7 @@ class TouchableComponent : public ActionableComponent { void setGestureHandlers(); PointerCaptureStatus processPointerEvent(const PointerEvent& event, apl_time_t timestamp, bool onlyProcessGestures) override; void invokeStandardAccessibilityAction(const std::string& name) override; + void getSupportedStandardAccessibilityActions(std::map& result) const override; private: void executePreEventActions(PropertyKey handlerKey); diff --git a/aplcore/include/apl/component/touchwrappercomponent.h b/aplcore/include/apl/component/touchwrappercomponent.h index 5f6a504..7fc12dd 100644 --- a/aplcore/include/apl/component/touchwrappercomponent.h +++ b/aplcore/include/apl/component/touchwrappercomponent.h @@ -35,7 +35,7 @@ class TouchWrapperComponent : public TouchableComponent { * @param component Pointer to cast. * @return Casted pointer to this type, nullptr if not possible. */ - static std::shared_ptr cast(const std::shared_ptr& component); + static std::shared_ptr cast(const ComponentPtr& component); /** * Inject component that will replace current child of touch wrapper. While likely short lived - it will exist for diff --git a/aplcore/include/apl/component/vectorgraphiccomponent.h b/aplcore/include/apl/component/vectorgraphiccomponent.h index 2aafd69..878245a 100644 --- a/aplcore/include/apl/component/vectorgraphiccomponent.h +++ b/aplcore/include/apl/component/vectorgraphiccomponent.h @@ -70,6 +70,16 @@ class VectorGraphicComponent : public TouchableComponent, bool fixMediaLayer(sg::SceneGraphUpdates& sceneGraph, const sg::LayerPtr& layer); #endif // SCENEGRAPH +private: + /** + * When providing parameters to the graphic, use this component's explicit parameters property, + * if specified. Otherwise fall back to exposing properties as implicit parameters. + */ + Properties getGraphicParameters() { + const auto& explicitParameters = getCalculated(kPropertyParameters); + return explicitParameters.empty() ? mProperties : Properties(explicitParameters); + } + private: bool mOnLoadOnFailReported = false; bool mGraphicReplaced = false; diff --git a/aplcore/include/apl/component/videocomponent.h b/aplcore/include/apl/component/videocomponent.h index 77139a9..138e973 100644 --- a/aplcore/include/apl/component/videocomponent.h +++ b/aplcore/include/apl/component/videocomponent.h @@ -57,7 +57,7 @@ class VideoComponent : public CoreComponent { * @param component Pointer to cast. * @return Casted pointer to this type, nullptr if not possible. */ - static std::shared_ptr cast(const std::shared_ptr& component); + static std::shared_ptr cast(const ComponentPtr& component); protected: const EventPropertyMap & eventPropertyMap() const override; diff --git a/aplcore/include/apl/content/aplversion.h b/aplcore/include/apl/content/aplversion.h index b8514d7..79470c5 100644 --- a/aplcore/include/apl/content/aplversion.h +++ b/aplcore/include/apl/content/aplversion.h @@ -38,6 +38,7 @@ class APLVersion { kAPLVersion20222 = 0x1U << 11, /// Support version 2022.2 kAPLVersion20231 = 0x1U << 12, /// Support version 2023.1 kAPLVersion20232 = 0x1U << 13, /// Support version 2023.2 + kAPLVersion20233 = 0x1U << 14, /// Support version 2023.3 kAPLVersion10to11 = kAPLVersion10 | kAPLVersion11, /// Convenience ranges from 1.0 to latest, kAPLVersion10to12 = kAPLVersion10to11 | kAPLVersion12, kAPLVersion10to13 = kAPLVersion10to12 | kAPLVersion13, @@ -51,9 +52,10 @@ class APLVersion { kAPLVersion20221to20222 = kAPLVersion10to20221 | kAPLVersion20222, kAPLVersion20222to20231 = kAPLVersion20221to20222 | kAPLVersion20231, kAPLVersion20231to20232 = kAPLVersion20222to20231 | kAPLVersion20232, - kAPLVersionLatest = kAPLVersion20231to20232, /// Support the most recent engine version - kAPLVersionDefault = kAPLVersion20231to20232, /// Default value - kAPLVersionReported = kAPLVersion20232, /// Default reported version + kAPLVersion20232to20233 = kAPLVersion20231to20232 | kAPLVersion20233, + kAPLVersionLatest = kAPLVersion20232to20233, /// Support the most recent engine version + kAPLVersionDefault = kAPLVersion20232to20233, /// Default value + kAPLVersionReported = kAPLVersion20233, /// Default reported version kAPLVersionAny = 0xffffffff, /// Support any versions in the list }; diff --git a/aplcore/include/apl/content/configurationchange.h b/aplcore/include/apl/content/configurationchange.h index 096e647..b089f83 100644 --- a/aplcore/include/apl/content/configurationchange.h +++ b/aplcore/include/apl/content/configurationchange.h @@ -44,19 +44,63 @@ class ConfigurationChange { ConfigurationChange(int pixelWidth, int pixelHeight) : mFlags(kConfigurationChangeSize), mPixelWidth(pixelWidth), - mPixelHeight(pixelHeight) - {} + mPixelHeight(pixelHeight), + mMinPixelWidth(pixelWidth), + mMaxPixelWidth(pixelWidth), + mMinPixelHeight(pixelHeight), + mMaxPixelHeight(pixelHeight) + { + assert(mPixelWidth >= 0); + assert(mPixelHeight >= 0); + } /** - * Update the size + * Update the size to a new fixed size * @param pixelWidth The pixel width of the screen * @param pixelHeight The pixel height of the screen * @return This object for chaining */ ConfigurationChange& size(int pixelWidth, int pixelHeight) { + assert(pixelWidth >= 0); + assert(pixelHeight >= 0); + + mFlags |= kConfigurationChangeSize; + mPixelWidth = pixelWidth; + mPixelHeight = pixelHeight; + mMinPixelWidth = pixelWidth; + mMinPixelHeight = pixelHeight; + mMaxPixelWidth = pixelWidth; + mMaxPixelHeight = pixelHeight; + return *this; + } + + /** + * Update the size to a new variable size + * @param pixelWidth The default pixel width of the view port + * @param minPixelWidth The minimum pixel width of the view port + * @param maxPixelWidth The maximum pixel width of the view port + * @param pixelHeight The default pixel height of the view port + * @param minPixelHeight The minimum pixel height of the view port + * @param maxPixelHeight The maximum pixel height of the view port + * @return This object for chaining + */ + ConfigurationChange& sizeRange(int pixelWidth, int minPixelWidth, int maxPixelWidth, + int pixelHeight, int minPixelHeight, int maxPixelHeight) { + assert(minPixelHeight >= 0); + assert(minPixelHeight <= pixelHeight); + assert(pixelHeight <= maxPixelHeight); + + assert(minPixelWidth >= 0); + assert(minPixelWidth <= pixelWidth); + assert(pixelWidth <= maxPixelWidth); + mFlags |= kConfigurationChangeSize; mPixelWidth = pixelWidth; mPixelHeight = pixelHeight; + mMinPixelWidth = minPixelWidth; + mMinPixelHeight = minPixelHeight; + mMaxPixelWidth = maxPixelWidth; + mMaxPixelHeight = maxPixelHeight; return *this; } @@ -187,6 +231,13 @@ class ConfigurationChange { */ RootConfig mergeRootConfig(const RootConfig& oldRootConfig) const; + /** + * Merge this configuration change into an environment object bag. + * @param oldEnvironment The old environment to merge with this change + * @return A new environment object bag. + */ + ObjectMap mergeEnvironment(const ObjectMap& oldEnvironment) const; + /** * Merge a new configuration change into this one. * @param other The old configuration change to merge with this change @@ -209,7 +260,16 @@ class ConfigurationChange { /** * @return New pixel size from this change. */ - Size getSize() const { return { static_cast(mPixelWidth), static_cast(mPixelHeight) }; } + ViewportSize getSize(double scale) const { + return { + static_cast(scale * mPixelWidth), + static_cast(scale * mMinPixelWidth), + static_cast(scale * mMaxPixelWidth), + static_cast(scale * mPixelHeight), + static_cast(scale * mMinPixelHeight), + static_cast(scale * mMaxPixelHeight) + }; + } /** * @return True if the configuration change is empty @@ -221,6 +281,16 @@ class ConfigurationChange { */ void clear() { mFlags = 0; } + /** + * @return Embedded document-suitable config change. + */ + ConfigurationChange embeddedDocumentChange() const; + + /** + * @return Theme contained in this change. + */ + const std::string& theme() const { return mTheme; } + /** * @return the full set of synthesized property names that can be added to events (e.g. "rotated"). */ @@ -243,6 +313,10 @@ class ConfigurationChange { // Metrics properties int mPixelWidth = 100; int mPixelHeight = 100; + int mMinPixelWidth = 100; + int mMaxPixelWidth = 100; + int mMinPixelHeight = 100; + int mMaxPixelHeight = 100; std::string mTheme = "dark"; ViewportMode mViewportMode = kViewportModeHub; diff --git a/aplcore/include/apl/content/content.h b/aplcore/include/apl/content/content.h index f28c691..18c3cf2 100644 --- a/aplcore/include/apl/content/content.h +++ b/aplcore/include/apl/content/content.h @@ -18,6 +18,7 @@ #include "apl/common.h" #include "apl/content/extensionrequest.h" +#include "apl/content/metrics.h" #include "apl/content/package.h" #include "apl/content/settings.h" #include "apl/engine/properties.h" @@ -30,7 +31,6 @@ class JsonData; class ImportRequest; class ImportRef; class RootConfig; -class Metrics; /** * Hold all of the documents and data necessary to inflate an APL component hierarchy. @@ -60,14 +60,18 @@ class Metrics; * with actual data sets. Use the addData() method to wire up parameter names * with JSON data. */ -class Content : public Counter { +class Content : public Counter, + public std::enable_shared_from_this { public: /** * Construct the working Content object from a document. * @param document The JSON document. * @return A pointer to the content or nullptr if invalid. + * @deprecated Use #create(JsonData&&, const SessionPtr&, const Metrics&, const RootConfig&) + * for root document and #create(JsonData&& document, const SessionPtr& session) for + * embedded document. */ - static ContentPtr create(JsonData&& document); + static APL_DEPRECATED ContentPtr create(JsonData&& document); /** * Construct the working Content object from a document, including a session for @@ -75,9 +79,35 @@ class Content : public Counter { * @param document The JSON document * @param session A logging session * @return A pointer to the content or nullptr if invalid + * @note Should be used only for Embedded documents. */ static ContentPtr create(JsonData&& document, const SessionPtr& session); + /** + * Construct the working Content object. + * @param document The JSON document + * @param session A logging session + * @param metrics Viewport metrics. + * @param config Document config. + * @return A pointer to the content or nullptr if invalid + */ + static ContentPtr create(JsonData&& document, const SessionPtr& session, + const Metrics& metrics, const RootConfig& config); + + /** + * Refresh content with new (or finally known) parameters. + * @param metrics Viewport metrics. + * @param config Document config. + */ + void refresh(const Metrics& metrics, const RootConfig& config); + + /** + * Refresh content with embedded document request. + * @param request request. + * @param documentConfig DocumentConfig. + */ + void refresh(const EmbedRequest& request, const DocumentConfigPtr& documentConfig); + /** * @return The main document package */ @@ -157,9 +187,18 @@ class Content : public Counter { /** * @return The background object (color or gradient) for this document. Returns * the transparent color if no background is defined. + * @deprecated Use #getBackground(). This method will create temporary evaluation context. */ Object getBackground(const Metrics& metrics, const RootConfig& config) const; + /** + * @return The background object (color or gradient) for this document. Returns + * the transparent color if no background is defined. + * @note Usable only if full (#create(JsonData&&, const SessionPtr&, const Metrics&, const RootConfig&)) + * constructor used as it requires stable evaluation context. + */ + Object getBackground() const; + /** * Returned object for the getEnvironment method. Defined as a structure for * future expansion. @@ -213,6 +252,11 @@ class Content : public Counter { */ std::set getPendingParameters() const { return mPendingParameters; } + /** + * @return True if content can change due to evaluation support, false otherwise. + */ + bool isMutable() const { return mEvaluationContext != nullptr; } + private: // Non-public methods used by other classes friend class CoreDocumentContext; @@ -229,17 +273,30 @@ class Content : public Counter { */ Content(const SessionPtr& session, const PackagePtr& mainPackagePtr, - const rapidjson::Value& mainTemplate); + const rapidjson::Value& mainTemplate, + const Metrics& metrics, + const RootConfig& rootConfig); private: // Private internal methods + void init(bool supportsEvaluation); void addImportList(Package& package); - void addImport(Package& package, const rapidjson::Value& value); + bool addImport( + Package& package, + const rapidjson::Value& value, + const std::string& name = "", + const std::string& version = "", + const std::set& loadAfter = {}); void addExtensions(Package& package); void updateStatus(); void loadExtensionSettings(); bool orderDependencyList(); bool addToDependencyList(std::vector& ordered, std::set& inProgress, const PackagePtr& package); bool allowAdd(const std::string& name); + std::string extractTheme(const Metrics& metrics) const; + static ContentPtr create(JsonData&& document, const SessionPtr& session, const Metrics& metrics, + const RootConfig& config, bool supportsEvaluation); + Object extractBackground(const Context& evaluationContext) const; + void loadPackage(const ImportRef& ref, const PackagePtr& package); private: enum State { @@ -257,10 +314,14 @@ class Content : public Counter { State mState; const rapidjson::Value& mMainTemplate; + Metrics mMetrics; + RootConfig mConfig; + ContextPtr mEvaluationContext; std::set mRequested; std::set mPending; std::map mLoaded; + std::map mStashed; std::vector mOrderedDependencies; std::map mParameterValues; diff --git a/aplcore/include/apl/content/documentconfig.h b/aplcore/include/apl/content/documentconfig.h index be117fa..f138915 100644 --- a/aplcore/include/apl/content/documentconfig.h +++ b/aplcore/include/apl/content/documentconfig.h @@ -18,6 +18,8 @@ #include "apl/common.h" +#include "apl/primitives/object.h" + #ifdef ALEXAEXTENSIONS #include #endif @@ -75,11 +77,30 @@ class DocumentConfig { */ const std::set& getDataSourceProviders() const { return mDataSources; } + /** + * Set Environment value for the document. + * @param name Environment key. + * @param value Environment value. + * @return This object for chaining. + */ + DocumentConfig& setEnvironmentValue(const std::string& name, const Object& value) { + mEnvironmentValues[name] = value; + return *this; + } + + /** + * @return Environment values. + */ + const ObjectMap& getEnvironmentValues() const { + return mEnvironmentValues; + } + private: #ifdef ALEXAEXTENSIONS ExtensionMediatorPtr mExtensionMediator; #endif std::set mDataSources; + ObjectMap mEnvironmentValues; }; } diff --git a/aplcore/include/apl/content/importref.h b/aplcore/include/apl/content/importref.h index 89119ff..5758657 100644 --- a/aplcore/include/apl/content/importref.h +++ b/aplcore/include/apl/content/importref.h @@ -17,6 +17,7 @@ #define _APL_IMPORT_REF_H #include +#include namespace apl { @@ -27,8 +28,19 @@ namespace apl { class ImportRef { public: ImportRef() {} - ImportRef(const std::string& name, const std::string& version) - : mName(name), mVersion(version) + + ImportRef( + const std::string& name, + const std::string& version) + : ImportRef(name, version, "", std::set()) + {} + + ImportRef( + const std::string& name, + const std::string& version, + const std::string& source, + const std::set& loadAfter) + : mName(name), mVersion(version), mSource(source), mLoadAfter(loadAfter) {} ImportRef(const ImportRef&) = default; @@ -38,6 +50,8 @@ class ImportRef { const std::string& name() const { return mName; } const std::string& version() const { return mVersion; } + const std::string& source() const { return mSource; } + const std::set& loadAfter() const { return mLoadAfter; } std::string toString() const { return mName + ":" + mVersion; } bool operator==(const ImportRef& other) const { return this->compare(other) == 0; } @@ -52,6 +66,8 @@ class ImportRef { private: std::string mName; std::string mVersion; + std::string mSource; + std::set mLoadAfter; }; } // namespace apl diff --git a/aplcore/include/apl/content/importrequest.h b/aplcore/include/apl/content/importrequest.h index 2f5294f..91a9ff4 100644 --- a/aplcore/include/apl/content/importrequest.h +++ b/aplcore/include/apl/content/importrequest.h @@ -18,7 +18,8 @@ #include -#include "importref.h" +#include "apl/common.h" +#include "apl/content/importref.h" #include "rapidjson/document.h" namespace apl { @@ -31,7 +32,26 @@ class Object; */ class ImportRequest { public: - ImportRequest(const rapidjson::Value& value); + /** + * Create ImportRequest. + * @param value JSON with package import specification. + * @param context data binding context. + * @param commonName name to be used if none specified in *value*. + * @param commonVersion version to be used if none specified in *value*. + * @param commonLoadAfter loadAfter to be used if none specified in *value*. + * @return ImportRequest. May be invalid, use #isValid() to check. + */ + static ImportRequest create(const rapidjson::Value& value, + const ContextPtr& context, + const std::string& commonName, + const std::string& commonVersion, + const std::set& commonLoadAfter); + + ImportRequest(); + ImportRequest(const std::string& name, + const std::string& version, + const std::string& source, + const std::set& loadAfter); ImportRequest(const ImportRequest&) = default; ImportRequest(ImportRequest&&) = default; @@ -45,14 +65,15 @@ class ImportRequest { bool operator!=(const ImportRequest& other) const { return this->compare(other) != 0; } bool operator<(const ImportRequest& other) const { return this->compare(other) < 0; } - int compare(const ImportRequest& other) const { - return mReference.compare(other.reference()); - } + int compare(const ImportRequest& other) const { return mReference.compare(other.reference()); } uint32_t getUniqueId() const { return mUniqueId; } - const std::string& source() const { return mSource; } + const std::string& source() const { return mReference.source(); } + + static std::pair extractNameAndVersion(const rapidjson::Value& value, const ContextPtr& context); + static std::set extractLoadAfter(const rapidjson::Value& value, const ContextPtr& context); + private: ImportRef mReference; - std::string mSource; bool mValid; uint32_t mUniqueId; static uint32_t sNextId; diff --git a/aplcore/include/apl/content/metrics.h b/aplcore/include/apl/content/metrics.h index e5b6a16..e3d5df6 100644 --- a/aplcore/include/apl/content/metrics.h +++ b/aplcore/include/apl/content/metrics.h @@ -18,6 +18,7 @@ #include +#include "apl/primitives/size.h" #include "apl/utils/streamer.h" #include "apl/utils/bimap.h" #include "apl/utils/log.h" @@ -54,6 +55,19 @@ enum ViewportMode { extern Bimap sViewportModeBimap; +struct ViewportSize { + float width, minWidth, maxWidth; + float height, minHeight, maxHeight; + + bool isFixed() const { return minWidth == maxWidth && minHeight == maxHeight; } + bool isAutoWidth() const { return minWidth != maxWidth; } + bool isAutoHeight() const { return minHeight != maxHeight; } + Size nominalSize() const { return { width, height }; } + Size layoutSize() const { + return {minWidth == maxWidth ? width : -1, minHeight == maxHeight ? height : -1}; + } +}; + /** * Store information about the viewport */ @@ -79,8 +93,8 @@ class Metrics : public UserData { /** * Set the pixel dimensions of the screen or view. When using auto-sizing, this * should be set to the nominal or target dimension of the view. - * @param pixelWidth The width of the screen, in pixels. - * @param pixelHeight The height of the screen, in pixels. + * @param pixelWidth The width of the viewport, in pixels. + * @param pixelHeight The height of the viewport, in pixels. * @return This object for chaining. */ Metrics& size(int pixelWidth, int pixelHeight) { @@ -91,22 +105,30 @@ class Metrics : public UserData { } /** - * Set if the width of the view can be automatically sized by the APL document - * @param value True if the view width can be auto-sized. + * Set the minimum and maximum pixel width of the viewport. + * @param minPixelWidth The minimum width of the viewport, in pixels + * @param maxPixelWidth The maximum width of the viewport, in pixels * @return This object for chaining */ - Metrics& autoSizeWidth(bool value) { - mAutoSizeWidth = value; + Metrics& minAndMaxWidth(int minPixelWidth, int maxPixelWidth) { + assert(minPixelWidth > 0 && minPixelWidth <= maxPixelWidth); + mMinPixelWidth = minPixelWidth; + mMaxPixelWidth = maxPixelWidth; + mFlags |= kMinMaxPixelWidth; return *this; } /** - * Set if the height of the view can be automatically sized by the APL document - * @param value True if the view height can be auto-sized. + * Set the minimum and maximum pixel height of the viewport. + * @param minPixelHeight The minimum height of the viewport, in pixels + * @param maxPixelHeight The maximum height of the viewport, in pixels * @return This object for chaining */ - Metrics& autoSizeHeight(bool value) { - mAutoSizeHeight = value; + Metrics& minAndMaxHeight(int minPixelHeight, int maxPixelHeight) { + assert(minPixelHeight > 0 && minPixelHeight <= maxPixelHeight); + mMinPixelHeight = minPixelHeight; + mMaxPixelHeight = maxPixelHeight; + mFlags |= kMinMaxPixelHeight; return *this; } @@ -172,43 +194,96 @@ class Metrics : public UserData { } /** - * @return The dpi of the screen. + * @return The dpi of the viewport. */ int getDpi() const { return mDpi; } /** - * @return The height of the screen in "dp" + * @return The complete viewport information needed for layout + */ + ViewportSize getViewportSize() const { + return { + getWidth(), + getMinWidth(), + getMaxWidth(), + getHeight(), + getMinHeight(), + getMaxHeight() + }; + } + + /** + * @return The height of the viewport in dp */ float getHeight() const { return pxToDp(mPixelHeight); } /** - * @return The width of the screen in "dp" + * @return The width of the viewport in dp */ float getWidth() const { return pxToDp(mPixelWidth); } + /** + * @return The minimum height of the viewport in dp + */ + float getMinHeight() const { + return pxToDp((mFlags & kMinMaxPixelHeight) != 0 ? mMinPixelHeight : mPixelHeight); + } + + /** + * @return The maximum height of the viewport in dp + */ + float getMaxHeight() const { + return pxToDp((mFlags & kMinMaxPixelHeight) != 0 ? mMaxPixelHeight : mPixelHeight); + } + + /** + * @return The minimum width of the viewport in dp + */ + float getMinWidth() const { + return pxToDp((mFlags & kMinMaxPixelWidth) != 0 ? mMinPixelWidth : mPixelWidth); + } + + /** + * @return The maximum height of the viewport in dp + */ + float getMaxWidth() const { + return pxToDp((mFlags & kMinMaxPixelWidth) != 0 ? mMaxPixelWidth : mPixelWidth); + } + /** * @return True if the width should auto-size */ - bool getAutoWidth() const { return mAutoSizeWidth; } + bool getAutoWidth() const { + return (mFlags & kMinMaxPixelWidth) != 0 && mMinPixelWidth < mMaxPixelWidth; + } /** * @return True if the height should auto-size */ - bool getAutoHeight() const { return mAutoSizeHeight; } + bool getAutoHeight() const { + return (mFlags & kMinMaxPixelHeight) != 0 && mMinPixelHeight < mMaxPixelHeight; + } /** * Convert Display Pixels to Pixels * @param dp Display Pixels * @return Pixels */ - float dpToPx(float dp) const { return dp * mDpi / CORE_DPI; } + float dpToPx(float dp) const { return dp * static_cast(mDpi) / CORE_DPI; } /** * Convert Pixels to Display Pixels * @param px Pixels * @return Display Pixels */ - float pxToDp(float px) const { return px * CORE_DPI / mDpi; } + float pxToDp(float px) const { return px * CORE_DPI / static_cast(mDpi); } + + /** + * Convert pixels to display pixels + * @param px Pixels + * @return Display pixels + */ + float pxToDp(int px) const { return static_cast(px) * CORE_DPI / static_cast(mDpi); } /** * @return The human-readable shape of the screen (either "rectangle" or "round") @@ -248,14 +323,25 @@ class Metrics : public UserData { std::string toDebugString() const; private: + + enum SetFlags : unsigned int { + kMinMaxPixelWidth = 1u << 0, + kMinMaxPixelHeight = 1u << 1, + }; + std::string mTheme = "dark"; int mPixelWidth = 1024; int mPixelHeight = 800; int mDpi = CORE_DPI; ScreenShape mShape = RECTANGLE; ViewportMode mMode = kViewportModeHub; - bool mAutoSizeWidth = false; - bool mAutoSizeHeight = false; + + int mMinPixelWidth = 1024; + int mMaxPixelWidth = 1024; + int mMinPixelHeight = 800; + int mMaxPixelHeight = 800; + + unsigned int mFlags = 0; }; } // namespace apl diff --git a/aplcore/include/apl/content/package.h b/aplcore/include/apl/content/package.h index da490e3..fa62b12 100644 --- a/aplcore/include/apl/content/package.h +++ b/aplcore/include/apl/content/package.h @@ -79,7 +79,6 @@ class Package : public Counter, friend class RootContext; void addDependency(const ImportRef& ref) { mDependencies.push_back(ref); } - void addDependency(ImportRef&& ref) { mDependencies.push_back(ref); } const std::vector& getDependencies() const { return mDependencies; } diff --git a/aplcore/include/apl/content/rootconfig.h b/aplcore/include/apl/content/rootconfig.h index 517e5bc..4bf468d 100644 --- a/aplcore/include/apl/content/rootconfig.h +++ b/aplcore/include/apl/content/rootconfig.h @@ -99,6 +99,8 @@ class RootConfig { kExperimentalFeatureRequestKeyboard, /// AVG should use layers for parameterized elements kExperimentalFeatureGraphicLayers, + /// Accessibility actions reported on component may depend on component state + kExperimentalFeatureDynamicAccessibilityActions }; /** @@ -197,6 +199,16 @@ class RootConfig { return *this; } + /** + * Specify the set of enabled experimental features for this rootConfig + * @param enabledExperimentalFeatures The set of enabled experimental features. + * @return This object for chaining + */ + RootConfig& experimentalFeatures(const std::set& enabledExperimentalFeatures){ + mEnabledExperimentalFeatures = enabledExperimentalFeatures; + return *this; + } + #ifdef SCENEGRAPH /** * Specify the edit text factory used for creating edit text objects. @@ -955,14 +967,16 @@ class RootConfig { std::shared_ptr getLocaleMethods() const { return mLocaleMethods; } /** + * @deprecated Use getProperty(RootProperty::kAgentName) instead * @return The agent name string */ - std::string getAgentName() const { return getProperty(RootProperty::kAgentName).getString(); } + APL_DEPRECATED std::string getAgentName() const { return getProperty(RootProperty::kAgentName).getString(); } /** + * @deprecated Use getProperty(RootProperty::kAgentVersion) instead * @return The agent version string */ - std::string getAgentVersion() const { return getProperty(RootProperty::kAgentVersion).getString(); } + APL_DEPRECATED std::string getAgentVersion() const { return getProperty(RootProperty::kAgentVersion).getString(); } /** * @return The expected animation quality @@ -977,14 +991,16 @@ class RootConfig { const char* getAnimationQualityString() const; /** + * @deprecated Use getProperty(RootProperty::kAllowOpenUrl) instead * @return True if the OpenURL command is supported */ - bool getAllowOpenUrl() const { return getProperty(RootProperty::kAllowOpenUrl).getBoolean(); } + APL_DEPRECATED bool getAllowOpenUrl() const { return getProperty(RootProperty::kAllowOpenUrl).getBoolean(); } /** + * @deprecated Use getProperty(RootProperty::kDisallowVideo) instead * @return True if the video component is not supported. */ - bool getDisallowVideo() const { return getProperty(RootProperty::kDisallowVideo).getBoolean(); } + APL_DEPRECATED bool getDisallowVideo() const { return getProperty(RootProperty::kDisallowVideo).getBoolean(); } /** * @return Time in ms for default IdleTimeout value. @@ -997,14 +1013,16 @@ class RootConfig { APLVersion getEnforcedAPLVersion() const { return mEnforcedAPLVersion; } /** + * @deprecated Use getProperty(RootProperty::kReportedVersion) instead * @return The reported version of APL used during document inflation */ - std::string getReportedAPLVersion() const { return getProperty(RootProperty::kReportedVersion).getString(); } + APL_DEPRECATED std::string getReportedAPLVersion() const { return getProperty(RootProperty::kReportedVersion).getString(); } /** + * @deprecated Use getProperty(RootProperty::kEnforceTypeField) instead * @return true if the type field value of an APL doc should be enforced */ - bool getEnforceTypeField() const { return getProperty(RootProperty::kEnforceTypeField).getBoolean(); } + APL_DEPRECATED bool getEnforceTypeField() const { return getProperty(RootProperty::kEnforceTypeField).getBoolean(); } /** * @return The default font color @@ -1027,14 +1045,16 @@ class RootConfig { } /** + * @deprecated Use getProperty(RootProperty::kDefaultFontFamily) instead * @return The default font family */ - std::string getDefaultFontFamily() const { return getProperty(RootProperty::kDefaultFontFamily).getString(); } + APL_DEPRECATED std::string getDefaultFontFamily() const { return getProperty(RootProperty::kDefaultFontFamily).getString(); } /** + * @deprecated Use getProperty(RootProperty::kTrackProvenance) instead * @return True if provenance of resources, styles, and components will be calculated. */ - bool getTrackProvenance() const { return getProperty(RootProperty::kTrackProvenance).getBoolean(); } + APL_DEPRECATED bool getTrackProvenance() const { return getProperty(RootProperty::kTrackProvenance).getBoolean(); } /** * Return the default width for this component type. @@ -1066,15 +1086,17 @@ class RootConfig { /** * Return number of pages to ensure around current page. + * @deprecated Use getProperty(RootProperty::kPagerChildCache) instead * @return number of pages. */ - int getPagerChildCache() const { return getProperty(RootProperty::kPagerChildCache).getInteger(); } + APL_DEPRECATED int getPagerChildCache() const { return getProperty(RootProperty::kPagerChildCache).getInteger(); } /** * Return number of pages to ensure around current one. + * @deprecated Use getProperty(RootProperty::kSequenceChildCache) instead * @return number of pages. */ - int getSequenceChildCache() const { return getProperty(RootProperty::kSequenceChildCache).getInteger(); } + APL_DEPRECATED int getSequenceChildCache() const { return getProperty(RootProperty::kSequenceChildCache).getInteger(); } /** * @return The current session pointer @@ -1083,16 +1105,18 @@ class RootConfig { APL_DEPRECATED SessionPtr getSession() const { return nullptr; } /** + * @deprecated Use getProperty(RootProperty::kUTCTime) instead * @return The starting UTC time in milliseconds past the epoch. */ - apl_time_t getUTCTime() const { return getProperty(RootProperty::kUTCTime).getDouble(); } + APL_DEPRECATED apl_time_t getUTCTime() const { return getProperty(RootProperty::kUTCTime).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kLocalTimeAdjustment) instead * @return The local time zone adjustment. This is the duration in milliseconds, which added * to the current time in UTC gives the local time. This includes any daylight saving * adjustment. */ - apl_duration_t getLocalTimeAdjustment() const { return getProperty(RootProperty::kLocalTimeAdjustment).getDouble(); } + APL_DEPRECATED apl_duration_t getLocalTimeAdjustment() const { return getProperty(RootProperty::kLocalTimeAdjustment).getDouble(); } /** * @return A reference to the map of live data sources @@ -1168,7 +1192,7 @@ class RootConfig { * @return Any extension clients registered with the RootConfig for legacy support. * @deprecated Extensions should be managed via ExtensionMediator */ - const std::map> getLegacyExtensionClients() const { + const std::map getLegacyExtensionClients() const { return mLegacyExtensionClients; } @@ -1253,105 +1277,120 @@ class RootConfig { } /** + * @deprecated Use getProperty(RootProperty::kDoublePressTimeout) instead * @return Double press timeout in milliseconds. */ - apl_duration_t getDoublePressTimeout() const { + APL_DEPRECATED apl_duration_t getDoublePressTimeout() const { return getProperty(RootProperty::kDoublePressTimeout).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kLongPressTimeout) instead * @return Long press timeout in milliseconds. */ - apl_duration_t getLongPressTimeout() const { + APL_DEPRECATED apl_duration_t getLongPressTimeout() const { return getProperty(RootProperty::kLongPressTimeout).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kPressedDuration) instead * @return Duration to show the "pressed" state of a component when programmatically invoked */ - apl_duration_t getPressedDuration() const { + APL_DEPRECATED apl_duration_t getPressedDuration() const { return getProperty(RootProperty::kPressedDuration).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kTapOrScrollTimeout) instead * @return Maximum time to wait before deciding that a touch event starts a scroll or paging gesture. */ - apl_duration_t getTapOrScrollTimeout() const { + APL_DEPRECATED apl_duration_t getTapOrScrollTimeout() const { return getProperty(RootProperty::kTapOrScrollTimeout).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kPointerSlopThreshold) instead * @return SwipeAway trigger distance threshold. */ - float getSwipeAwayTriggerDistanceThreshold() const { + APL_DEPRECATED float getSwipeAwayTriggerDistanceThreshold() const { return getProperty(RootProperty::kPointerSlopThreshold).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kSwipeAwayFulfillDistancePercentageThreshold) instead * @return SwipeAway fulfill distance threshold in percents. */ - float getSwipeAwayFulfillDistancePercentageThreshold() const { + APL_DEPRECATED float getSwipeAwayFulfillDistancePercentageThreshold() const { return getProperty(RootProperty::kSwipeAwayFulfillDistancePercentageThreshold).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kSwipeAwayAnimationEasing) instead * @return Animation easing for SwipeAway gesture. */ - EasingPtr getSwipeAwayAnimationEasing() const; + APL_DEPRECATED EasingPtr getSwipeAwayAnimationEasing() const; /** + * @deprecated Use getProperty(RootProperty::kSwipeVelocityThreshold) instead * @return Swipe velocity threshold. */ - float getSwipeVelocityThreshold() const { + APL_DEPRECATED float getSwipeVelocityThreshold() const { return getProperty(RootProperty::kSwipeVelocityThreshold).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kSwipeMaxVelocity) instead * @return Maximum swipe velocity. */ - float getSwipeMaxVelocity() const { + APL_DEPRECATED float getSwipeMaxVelocity() const { return getProperty(RootProperty::kSwipeMaxVelocity).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kSwipeAngleTolerance) instead * @return Max allowed pointer movement angle during swipe gestures, as a slope. */ - float getSwipeAngleSlope() const { + APL_DEPRECATED float getSwipeAngleSlope() const { return getProperty(RootProperty::kSwipeAngleTolerance).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kDefaultSwipeAnimationDuration) instead * @return Default animation duration for SwipeAway gestures, in ms. */ - apl_duration_t getDefaultSwipeAnimationDuration() const { + APL_DEPRECATED apl_duration_t getDefaultSwipeAnimationDuration() const { return getProperty(RootProperty::kDefaultSwipeAnimationDuration).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kMaxSwipeAnimationDuration) instead * @return Max animation duration for SwipeAway gestures, in ms. */ - apl_duration_t getMaxSwipeAnimationDuration() const { + APL_DEPRECATED apl_duration_t getMaxSwipeAnimationDuration() const { return getProperty(RootProperty::kMaxSwipeAnimationDuration).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kMinimumFlingVelocity) instead * @return Fling velocity threshold */ - float getMinimumFlingVelocity() const { + APL_DEPRECATED float getMinimumFlingVelocity() const { return getProperty(RootProperty::kMinimumFlingVelocity).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kTickHandlerUpdateLimit) instead * @return Tick handler update limit. */ - apl_duration_t getTickHandlerUpdateLimit() const { + APL_DEPRECATED apl_duration_t getTickHandlerUpdateLimit() const { return getProperty(RootProperty::kTickHandlerUpdateLimit).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kFontScale) instead * @return The requested scaling factor for fonts */ - float getFontScale() const { + APL_DEPRECATED float getFontScale() const { return getProperty(RootProperty::kFontScale).getDouble(); } @@ -1370,51 +1409,58 @@ class RootConfig { } /** + * @deprecated Use getProperty(RootProperty::kScreenReader) instead * @return True if an accessibility screen reader is enabled */ - bool getScreenReaderEnabled() const { + APL_DEPRECATED bool getScreenReaderEnabled() const { return getProperty(RootProperty::kScreenReader).getBoolean(); } /** + * @deprecated Use getProperty(RootProperty::kPointerInactivityTimeout) instead * @return Pointer inactivity timeout. */ - apl_duration_t getPointerInactivityTimeout() const { + APL_DEPRECATED apl_duration_t getPointerInactivityTimeout() const { return getProperty(RootProperty::kPointerInactivityTimeout).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kMaximumFlingVelocity) instead * @return Maximum fling speed. */ - float getMaximumFlingVelocity() const { + APL_DEPRECATED float getMaximumFlingVelocity() const { return getProperty(RootProperty::kMaximumFlingVelocity).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kPointerSlopThreshold) instead * @return Pointer slop threshold. */ - float getPointerSlopThreshold() const { + APL_DEPRECATED float getPointerSlopThreshold() const { return getProperty(RootProperty::kPointerSlopThreshold).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kScrollCommandDuration) instead * @return Scroll command duration. */ - apl_duration_t getScrollCommandDuration() const { + APL_DEPRECATED apl_duration_t getScrollCommandDuration() const { return getProperty(RootProperty::kScrollCommandDuration).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kScrollSnapDuration) instead * @return Scroll snap duration. */ - apl_duration_t getScrollSnapDuration() const { + APL_DEPRECATED apl_duration_t getScrollSnapDuration() const { return getProperty(RootProperty::kScrollSnapDuration).getDouble(); } /** + * @deprecated Use getProperty(RootProperty::kDefaultPagerAnimationDuration) instead * @return Default pager animation duration. */ - apl_duration_t getDefaultPagerAnimationDuration() const { + APL_DEPRECATED apl_duration_t getDefaultPagerAnimationDuration() const { return getProperty(RootProperty::kDefaultPagerAnimationDuration).getDouble(); } @@ -1426,6 +1472,13 @@ class RootConfig { return mEnabledExperimentalFeatures.count(feature) > 0; } + /** + * @return set of enabled experimental features. + */ + const std::set getExperimentalFeatures() const{ + return mEnabledExperimentalFeatures; + } + /** * Create a new RootConfig instance, copying all non-document-specific state from this instance. * @return the copy @@ -1467,7 +1520,7 @@ class RootConfig { // Clients should only be created by the mediator, but for legacy reasons clients can be created // on their own. So, we need to keep track of these "standalone" clients so that we can extract // registration information from them later. - std::map> mLegacyExtensionClients; + std::map mLegacyExtensionClients; ObjectMap mSupportedExtensions; // URI -> config ObjectMap mExtensionFlags; // URI -> opaque flags @@ -1487,7 +1540,7 @@ class RootConfig { // Set of enabled experimental features std::set mEnabledExperimentalFeatures; RootPropertyMap mProperties; -}; + }; } diff --git a/aplcore/include/apl/content/rootproperties.h b/aplcore/include/apl/content/rootproperties.h index 6949c13..c377302 100644 --- a/aplcore/include/apl/content/rootproperties.h +++ b/aplcore/include/apl/content/rootproperties.h @@ -23,7 +23,13 @@ namespace apl { // Note: If any per-document properties added here, also update sCopyableConfigProperties +// Note: If any configurable property added here, also update sRootPropertyBimap +// Note: RootProperty must be added after kRootPropertySetBegin and before kRootPropertySetEnd, +// since they are tested for completeness enum RootProperty { + /// The Begin key marks the beginning of the enum members. + /// All new enum values should be added *after* this + kRootPropertySetBegin, /// Agent name kAgentName, /// Agent version @@ -137,6 +143,9 @@ enum RootProperty { kTextMeasurementCacheLimit, /// Initial display state of the document, used by core prior to any display state updates kInitialDisplayState, + /// The End key marks the end of the enum members. + /// All new enum values should be added *before* this + kRootPropertySetEnd }; extern Bimap sRootPropertyBimap; diff --git a/aplcore/include/apl/content/settings.h b/aplcore/include/apl/content/settings.h index c9e7d1d..139e8e6 100644 --- a/aplcore/include/apl/content/settings.h +++ b/aplcore/include/apl/content/settings.h @@ -112,7 +112,7 @@ class Settings { /** * @deprecated use Content->getDocumentSettings() */ - void read(const RootConfig& config) { + APL_DEPRECATED void read(const RootConfig& config) { mDefaultIdleTimeout = config.getDefaultIdleTimeout(); } diff --git a/aplcore/include/apl/datasource/datasourceconnection.h b/aplcore/include/apl/datasource/datasourceconnection.h index 360ec45..6403de3 100644 --- a/aplcore/include/apl/datasource/datasourceconnection.h +++ b/aplcore/include/apl/datasource/datasourceconnection.h @@ -48,7 +48,7 @@ class DataSourceConnection : public Counter { * * @return LiveArray used as data storage for this connection. */ - virtual std::shared_ptr getLiveArray() = 0; + virtual LiveArrayPtr getLiveArray() = 0; /** * Retrieve datasource context as a JSON object. Should be called by RootContext->serializeDatasourceContext() diff --git a/aplcore/include/apl/datasource/offsetindexdatasourceconnection.h b/aplcore/include/apl/datasource/offsetindexdatasourceconnection.h index 217faa8..bd1bdd6 100644 --- a/aplcore/include/apl/datasource/offsetindexdatasourceconnection.h +++ b/aplcore/include/apl/datasource/offsetindexdatasourceconnection.h @@ -56,7 +56,7 @@ class OffsetIndexDataSourceConnection : public DataSourceConnection { */ void ensure(size_t index) override; - std::shared_ptr getLiveArray() override; + LiveArrayPtr getLiveArray() override; /** * Provide an update to underlying data. diff --git a/aplcore/include/apl/document/coredocumentcontext.h b/aplcore/include/apl/document/coredocumentcontext.h index f4917f0..3ccfe20 100644 --- a/aplcore/include/apl/document/coredocumentcontext.h +++ b/aplcore/include/apl/document/coredocumentcontext.h @@ -55,22 +55,6 @@ class CoreDocumentContext : public DocumentContext, public std::enable_shared_fr const ContentPtr& content, const RootConfig& config); - /** - * Create a DocumentContext for the given Content with the given environment and size. - * - * @param context The Context in which the DocumentContext is created; ...not the Context of the DocumentContext - * @param content Represents the document rendered within the created DocumentContext - * @param env May contain overrides for context to be applied within the created DocumentContext - * @param size Specifies the height and width in display-independent pixels of the DocumentContext - * @param documentConfig Document configuration - * @return The DocumentContext - */ - static CoreDocumentContextPtr create(const ContextPtr& context, - const ContentPtr& content, - const Object& env, - const Size& size, - const DocumentConfigPtr& documentConfig); - /** * Notify core of a configuration change. Internally this method will trigger the "onConfigChange" * event handler in the APL document. A common behavior in the onConfigChange event handler is to @@ -78,8 +62,9 @@ class CoreDocumentContext : public DocumentContext, public std::enable_shared_fr * @see RootContext::configurationChange * * @param change Configuration change information + * @param embedded true if embedded document change, false otherwise. */ - void configurationChange(const ConfigurationChange& change); + void configurationChange(const ConfigurationChange& change, bool embedded); /** * Update the display state of the document. Internally this method will trigger the @@ -101,6 +86,26 @@ class CoreDocumentContext : public DocumentContext, public std::enable_shared_fr */ bool reinflate(const LayoutCallbackFunc& layoutCallback); + /** + * Start document reinflation. Extracts relevant status and stops any current processing. + * @param preservedSequencers output for sequencers to preserve. + * @return pair of success boolean and old top component, if any. + */ + std::pair startReinflate(std::map& preservedSequencers); + + /** + * Finish document reinflation. Relies on #startReinflate being called beforehand. + * @param layoutCallback Callback executed when reinflation process finished. + * @param oldTop Old top component, if any. + * @param preservedSequencers Sequencers that were preserved, if any. + * + * @return true if successful, false otherwise. + */ + bool finishReinflate( + const LayoutCallbackFunc& layoutCallback, + const CoreComponentPtr& oldTop, + const std::map& preservedSequencers); + /** * Trigger a resize based on stored configuration changes. * @see RootContext::resize @@ -244,6 +249,17 @@ class CoreDocumentContext : public DocumentContext, public std::enable_shared_fr void flushDataUpdates(); + /** + * Refresh content evaluation state. + * @return true if content requires resolution, after refresh, false otherwise. + */ + bool refreshContent(); + + const ConfigurationChange& activeChanges() const { return mActiveConfigurationChanges; } + + const Metrics& currentMetrics() const; + const RootConfig& currentConfig() const; + /** * @param documentContext Pointer to cast. * @return Casted pointer to this type @@ -271,9 +287,9 @@ class CoreDocumentContext : public DocumentContext, public std::enable_shared_fr const SharedContextDataPtr& sharedData, bool reinflation); - bool verifyAPLVersionCompatibility(const std::vector>& ordered, + bool verifyAPLVersionCompatibility(const std::vector& ordered, const APLVersion& compatibilityVersion); - bool verifyTypeField(const std::vector>& ordered, bool enforce); + bool verifyTypeField(const std::vector& ordered, bool enforce); ObjectMapPtr createDocumentEventProperties(const std::string& handler) const; private: @@ -283,6 +299,7 @@ class CoreDocumentContext : public DocumentContext, public std::enable_shared_fr ContextPtr mContext; DocumentContextDataPtr mCore; // When you die, make sure to tell the data to terminate itself. ConfigurationChange mActiveConfigurationChanges; + ConfigurationChange mResultingConfigurationChange; DisplayState mDisplayState; apl_time_t mUTCTime; diff --git a/aplcore/include/apl/document/documentcontextdata.h b/aplcore/include/apl/document/documentcontextdata.h index 1688088..3989835 100644 --- a/aplcore/include/apl/document/documentcontextdata.h +++ b/aplcore/include/apl/document/documentcontextdata.h @@ -173,7 +173,7 @@ class DocumentContextData : public ContextData, public std::enable_shared_from_t private: SharedContextDataPtr mSharedData; - std::weak_ptr mDocument; + DocumentContextWeakPtr mDocument; Metrics mMetrics; std::map mLayouts; std::map mCommands; diff --git a/aplcore/include/apl/embed/embedrequest.h b/aplcore/include/apl/embed/embedrequest.h index e0c17ca..d85a9cf 100644 --- a/aplcore/include/apl/embed/embedrequest.h +++ b/aplcore/include/apl/embed/embedrequest.h @@ -28,18 +28,20 @@ namespace apl { */ class EmbedRequest final { public: + static EmbedRequestPtr create(URLRequest url, const DocumentContextPtr& origin, const ComponentPtr& originComponent); - static EmbedRequestPtr create(URLRequest url, const DocumentContextPtr& origin); - - EmbedRequest(URLRequest url, const DocumentContextPtr& origin); + EmbedRequest(URLRequest url, const DocumentContextPtr& origin, const ComponentPtr& parent); const URLRequest& getUrlRequest() const; DocumentContextPtr getOrigin() const; private: + friend class Content; + const URLRequest mUrl; - std::weak_ptr mOrigin; + DocumentContextWeakPtr mOrigin; + std::weak_ptr mOriginComponent; }; } // namespace apl diff --git a/aplcore/include/apl/engine/builder.h b/aplcore/include/apl/engine/builder.h index 89544a2..7904e18 100644 --- a/aplcore/include/apl/engine/builder.h +++ b/aplcore/include/apl/engine/builder.h @@ -21,6 +21,7 @@ namespace apl { class Path; +class InsertItemCommand; using MakeComponentFunc = std::function; @@ -30,6 +31,7 @@ using MakeComponentFunc = std::function, static ContextPtr createTestContext(const Metrics& metrics, const RootConfig& config, const SessionPtr& session); /** - * Create a top-level context for the document background extraction. + * Create a top-level context for the Content evaluation. * @param metrics Display metrics. * @param config Root configuration * @param theme Theme * @param session Session * @return The context */ - static ContextPtr createBackgroundEvaluationContext( + static ContextPtr createContentEvaluationContext( const Metrics& metrics, const RootConfig& config, const std::string& aplVersion, diff --git a/aplcore/include/apl/engine/corerootcontext.h b/aplcore/include/apl/engine/corerootcontext.h index 12e6c65..d857141 100644 --- a/aplcore/include/apl/engine/corerootcontext.h +++ b/aplcore/include/apl/engine/corerootcontext.h @@ -26,6 +26,14 @@ namespace apl { */ class CoreRootContext : public std::enable_shared_from_this, public RootContext { public: + /** + * Construct a top-level root context. + * @param metrics Display metrics + * @param content Content to display + * @param config Configuration information + * @param callback Pre-layout callback + * @return A pointer to the root context. + */ static RootContextPtr create(const Metrics& metrics, const ContentPtr& content, const RootConfig& config, @@ -101,8 +109,7 @@ class CoreRootContext : public std::enable_shared_from_this, pu void mediaLoadFailed(const std::string& source, int errorCode = -1, const std::string& error = std::string()) override; - bool getAutoWidth() const; - bool getAutoHeight() const; + Size getViewportSize() const override; #ifdef SCENEGRAPH sg::SceneGraphPtr getSceneGraph() override; @@ -138,6 +145,13 @@ class CoreRootContext : public std::enable_shared_from_this, pu */ double getPxToDp() const; + /** + * Update the viewport size + * @param width Width of the viewport in dp + * @param height Height of the viewport in dp + */ + void setViewportSize(float width, float height) const; + private: friend class CoreDocumentContext; friend class ExtensionClient; // TODO: Required for backwards compatibility with V1 extension interface @@ -167,6 +181,7 @@ class CoreRootContext : public std::enable_shared_from_this, pu apl_duration_t mLocalTimeAdjustment = 0; DisplayState mDisplayState; CoreDocumentContextPtr mTopDocument; + mutable Size mViewportSize; // Viewport size in dp; mutable so that LayoutManager can change it #ifdef SCENEGRAPH sg::SceneGraphPtr mSceneGraph; #endif // SCENEGRAPH diff --git a/aplcore/include/apl/engine/event.h b/aplcore/include/apl/engine/event.h index 93e918b..97f5748 100644 --- a/aplcore/include/apl/engine/event.h +++ b/aplcore/include/apl/engine/event.h @@ -201,6 +201,12 @@ enum EventType { * Does not have an ActionRef */ kEventTypeOpenKeyboard, + + /** + * Document config requires to be refreshed. This usually includes checking if content isWaiting() + * and subsequently resolving required packages. + */ + kEventTypeContentRefresh, }; enum EventProperty { @@ -370,10 +376,10 @@ class Event : public UserData { * Tag the event with originating document. Called internally. * @param document originating document. */ - void setDocument(const std::weak_ptr& document) { mDocument = document;} + void setDocument(const DocumentContextWeakPtr& document) { mDocument = document;} std::shared_ptr mData; - std::weak_ptr mDocument; + DocumentContextWeakPtr mDocument; }; } // namespace apl diff --git a/aplcore/include/apl/engine/layoutmanager.h b/aplcore/include/apl/engine/layoutmanager.h index effd480..ef4a2a2 100644 --- a/aplcore/include/apl/engine/layoutmanager.h +++ b/aplcore/include/apl/engine/layoutmanager.h @@ -21,6 +21,7 @@ #include "apl/common.h" #include "apl/component/componentproperties.h" +#include "apl/content/metrics.h" #include "apl/primitives/object.h" #include "apl/primitives/size.h" @@ -68,7 +69,7 @@ class LayoutManager { * @param coreRootContext the CoreRootContext for which layouts will be managed * @param size Initial configured size. */ - LayoutManager(const CoreRootContext& coreRootContext, const Size& size); + LayoutManager(const CoreRootContext& coreRootContext, ViewportSize size); /** * Stop all layout processing (and future layout processing) @@ -79,7 +80,7 @@ class LayoutManager { * Set new viewport size * @param size new viewport size */ - void setSize(const Size& size); + void setSize(ViewportSize size); /** * @return True if there are components that need a layout pass @@ -117,7 +118,7 @@ class LayoutManager { * * @return @c true if the node is a top node, @c false otherwise */ - bool isTopNode(const std::shared_ptr& component) const; + bool isTopNode(const ConstCoreComponentPtr& component) const; /** * Mark this component as the top of a Yoga hierarchy @@ -167,6 +168,16 @@ class LayoutManager { */ void needToReProcessLayoutChanges() { mNeedToReProcessLayoutChanges = true; } + /** + * @return Suggested min and max width for provided component. + */ + std::pair getMinMaxWidth(const CoreComponent& component) const; + + /** + * @return Suggested min and max height for provided component. + */ + std::pair getMinMaxHeight(const CoreComponent& component) const; + using PPKey = std::pair, PropertyKey>; class PPKeyLess final { public: @@ -187,7 +198,7 @@ class LayoutManager { private: const CoreRootContext& mRoot; std::set mPendingLayout; - Size mConfiguredSize; + ViewportSize mConfiguredSize; bool mTerminated = false; bool mInLayout = false; // Guard against recursive calls to layout bool mNeedToReProcessLayoutChanges = false; diff --git a/aplcore/include/apl/engine/propdef.h b/aplcore/include/apl/engine/propdef.h index e700e7c..8a9a1a4 100644 --- a/aplcore/include/apl/engine/propdef.h +++ b/aplcore/include/apl/engine/propdef.h @@ -229,6 +229,8 @@ enum PropertyDefFlags : uint32_t { kPropTextHash = 0x4000, /// This property takes part in visual hash kPropVisualHash = 0x8000, + /// Property affects accessibility state of the component + kPropAccessibility = 0x10000, }; /** diff --git a/aplcore/include/apl/engine/recalculatesource.h b/aplcore/include/apl/engine/recalculatesource.h index a844725..efad83b 100644 --- a/aplcore/include/apl/engine/recalculatesource.h +++ b/aplcore/include/apl/engine/recalculatesource.h @@ -37,7 +37,7 @@ class RecalculateSource { * @param key The key of the local element. When this element is changed, the downstream dependant should recalculate. * @param dependant The dependant object connecting to the downstream dependant object. */ - void addDownstream(T key, const std::shared_ptr& dependant) { + void addDownstream(T key, const DependantPtr& dependant) { // For now, we strip off the "/" section of the keys auto name = key.substr(0, key.find("/", 0)); @@ -67,7 +67,7 @@ class RecalculateSource { * any released weak_ptrs at the same time. * @param dependant The object to remove */ - void removeDownstream(const std::shared_ptr& dependant) { + void removeDownstream(const DependantPtr& dependant) { auto it = mDownstream.begin(); while (it != mDownstream.end()) { // TODO: Possible optimization by using owner_before comparison diff --git a/aplcore/include/apl/engine/recalculatetarget.h b/aplcore/include/apl/engine/recalculatetarget.h index ece765f..8da5b16 100644 --- a/aplcore/include/apl/engine/recalculatetarget.h +++ b/aplcore/include/apl/engine/recalculatetarget.h @@ -43,7 +43,7 @@ class RecalculateTarget { * @param key The key the target downstream property that will be recalculated. * @param dependant The dependant object. */ - void addUpstream(T key, const std::shared_ptr& dependant) { + void addUpstream(T key, const DependantPtr& dependant) { mUpstream.emplace(key, dependant); } @@ -125,7 +125,7 @@ class RecalculateTarget { virtual void setValue(T key, const Object& value, bool useDirtyFlag) = 0; private: - std::multimap> mUpstream; + std::multimap mUpstream; }; } // namespace apl diff --git a/aplcore/include/apl/engine/rootcontext.h b/aplcore/include/apl/engine/rootcontext.h index f4ea477..f43cc43 100644 --- a/aplcore/include/apl/engine/rootcontext.h +++ b/aplcore/include/apl/engine/rootcontext.h @@ -464,6 +464,11 @@ class RootContext : public UserData, */ virtual void mediaLoadFailed(const std::string& source, int errorCode = -1, const std::string& error = std::string()) = 0; + /** + * @return The size of the viewport, in dp + */ + virtual Size getViewportSize() const = 0; + #ifdef SCENEGRAPH /** * This method returns the current scene graph. It will clear all dirty properties as well. diff --git a/aplcore/include/apl/engine/sharedcontextdata.h b/aplcore/include/apl/engine/sharedcontextdata.h index 86e6dc4..5663f6c 100644 --- a/aplcore/include/apl/engine/sharedcontextdata.h +++ b/aplcore/include/apl/engine/sharedcontextdata.h @@ -19,7 +19,6 @@ #include #include "apl/common.h" -#include "apl/content/content.h" #include "apl/content/metrics.h" #include "apl/content/settings.h" #include "apl/engine/event.h" @@ -185,8 +184,8 @@ class SharedContextData : public NonCopyable, public Counter, const DocumentManagerPtr mDocumentManager; std::shared_ptr mTimeManager; - std::shared_ptr mMediaManager; - std::shared_ptr mMediaPlayerFactory; + MediaManagerPtr mMediaManager; + MediaPlayerFactoryPtr mMediaPlayerFactory; YGConfigRef mYGConfigRef; TextMeasurementPtr mTextMeasurement; diff --git a/aplcore/include/apl/extension/extensionclient.h b/aplcore/include/apl/extension/extensionclient.h index efa137e..b84f00d 100644 --- a/aplcore/include/apl/extension/extensionclient.h +++ b/aplcore/include/apl/extension/extensionclient.h @@ -332,7 +332,7 @@ class ExtensionClient : public Counter, ParsedExtensionSchema mSchema; SessionPtr mSession; Object mFlags; - std::shared_ptr mInternalRootConfig; + RootConfigPtr mInternalRootConfig; std::string mConnectionToken; std::map mLiveData; std::map mActionRefs; diff --git a/aplcore/include/apl/extension/extensioncomponent.h b/aplcore/include/apl/extension/extensioncomponent.h index b6e336b..db794fe 100644 --- a/aplcore/include/apl/extension/extensioncomponent.h +++ b/aplcore/include/apl/extension/extensioncomponent.h @@ -92,7 +92,7 @@ class ExtensionComponent : public CoreComponent { * @param component Pointer to cast. * @return Casted pointer to this type, nullptr if not possible. */ - static std::shared_ptr cast(const std::shared_ptr& component); + static ExtensionComponentPtr cast(const ComponentPtr& component); protected: /** diff --git a/aplcore/include/apl/extension/extensionmediator.h b/aplcore/include/apl/extension/extensionmediator.h index 89cba34..4a22235 100644 --- a/aplcore/include/apl/extension/extensionmediator.h +++ b/aplcore/include/apl/extension/extensionmediator.h @@ -24,8 +24,7 @@ #include -#include "apl/content/content.h" -#include "apl/content/rootconfig.h" +#include "apl/common.h" #include "apl/document/displaystate.h" #include "apl/extension/extensionclient.h" #include "apl/extension/extensionsession.h" @@ -135,28 +134,31 @@ class ExtensionMediator : public std::enable_shared_from_this * @deprecated Use ExtensionMediator::initializeExtensions(const ObjectMap& flagMap, const * ContentPtr& content, const ExtensionGrantRequestCallback& grantHandler) instead */ - void initializeExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, - const ExtensionGrantRequestCallback& grantHandler = nullptr); + APL_DEPRECATED void initializeExtensions(const RootConfigPtr& rootConfig, + const ContentPtr& content, + const ExtensionGrantRequestCallback& grantHandler = nullptr); /** * @deprecated Use ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& * content, ExtensionsLoadedCallbackV2 loaded) instead */ - void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, ExtensionsLoadedCallback loaded); + APL_DEPRECATED void loadExtensions(const RootConfigPtr& rootConfig, + const ContentPtr& content, + ExtensionsLoadedCallback loaded); /** * @deprecated Use ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& * content, ExtensionsLoadedCallbackV2 loaded) instead */ - void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, - ExtensionsLoadedCallbackV2 loaded); + APL_DEPRECATED void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, + ExtensionsLoadedCallbackV2 loaded); /** * @deprecated Use ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& * content, const std::set* grantedExtensions) instead */ - void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, - const std::set* grantedExtensions = nullptr); + APL_DEPRECATED void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, + const std::set* grantedExtensions = nullptr); /** * Initialize extensions available in provided content. Performance gains can be made by @@ -294,6 +296,11 @@ class ExtensionMediator : public std::enable_shared_from_this */ void onDisplayStateChanged(DisplayState displayState); + /** + * @return a map of loaded extension uris to activity descriptors. + */ + std::unordered_map getLoadedExtensions(); + private: friend class CoreDocumentContext; friend class ExtensionManager; @@ -417,7 +424,7 @@ class ExtensionMediator : public std::enable_shared_from_this // session extracted from loaded content SessionPtr mSession; // retro extension wrapper used for message passing - std::map> mClients; + std::map mClients; // Determines whether incoming messages from extensions should be processed. bool mEnabled = true; // Pending Extension grants diff --git a/aplcore/include/apl/focus/focusmanager.h b/aplcore/include/apl/focus/focusmanager.h index 999aec4..b6cd9fb 100644 --- a/aplcore/include/apl/focus/focusmanager.h +++ b/aplcore/include/apl/focus/focusmanager.h @@ -117,8 +117,9 @@ class FocusManager { * Assign focus to this component. * @param component The component to focus. If null, will clear the focus. * @param notifyViewhost Flag to identify if viewhost notification required for this focus change. + * @param shouldScrollIntoView scroll the focused component into view if true, do nothing otherwise. */ - void setFocus(const CoreComponentPtr& component, bool notifyViewhost); + void setFocus(const CoreComponentPtr& component, bool notifyViewhost, bool shouldScrollIntoView = true); /** * Release the focus if it is set on this component. @@ -140,10 +141,16 @@ class FocusManager { */ CoreComponentPtr getFocus() { return mFocused.lock(); } + /** + * Terminates the Focus sequencers and prevents any further operations. + */ + void terminate(); + private: const CoreRootContext& mCore; std::unique_ptr mFinder; std::weak_ptr mFocused; + bool mTerminated = false; void reportFocusedComponent(); void clearFocusedComponent(); diff --git a/aplcore/include/apl/graphic/graphic.h b/aplcore/include/apl/graphic/graphic.h index fe145be..32b1999 100644 --- a/aplcore/include/apl/graphic/graphic.h +++ b/aplcore/include/apl/graphic/graphic.h @@ -54,7 +54,7 @@ class Graphic : public UIDObject, static GraphicPtr create(const ContextPtr& context, const JsonResource& jsonResource, Properties&& properties, - const std::shared_ptr& component); + const CoreComponentPtr& component); /** * Construct a graphic from raw JSON data @@ -67,7 +67,7 @@ class Graphic : public UIDObject, static GraphicPtr create(const ContextPtr& context, const rapidjson::Value& json, Properties&& properties, - const std::shared_ptr& component, + const CoreComponentPtr& component, const Path& path, const StyleInstancePtr& styledPtr = nullptr); /** @@ -189,7 +189,7 @@ class Graphic : public UIDObject, * Assign this graphic to a VectorGraphicComponent * @param component The component */ - void setComponent(const std::shared_ptr& component) { + void setComponent(const CoreComponentPtr& component) { mComponent = component; } @@ -201,7 +201,7 @@ class Graphic : public UIDObject, void initialize(const ContextPtr &sourceContext, const rapidjson::Value &json, Properties &&properties, - const std::shared_ptr &component, + const CoreComponentPtr &component, const Path& path, const StyleInstancePtr &styledPtr); void addDirtyChild(const GraphicElementPtr& child); diff --git a/aplcore/include/apl/media/coremediamanager.h b/aplcore/include/apl/media/coremediamanager.h index 98d976d..53d5d98 100644 --- a/aplcore/include/apl/media/coremediamanager.h +++ b/aplcore/include/apl/media/coremediamanager.h @@ -54,7 +54,7 @@ class CoreMediaManager : public MediaManager, public std::enable_shared_from_thi protected: std::map> mObjectMap; - std::set, std::owner_less>> mPending; + WeakPtrSet mPending; }; } // namespace apl diff --git a/aplcore/include/apl/media/mediatrack.h b/aplcore/include/apl/media/mediatrack.h index 5ac7bc2..6306d6a 100644 --- a/aplcore/include/apl/media/mediatrack.h +++ b/aplcore/include/apl/media/mediatrack.h @@ -21,6 +21,8 @@ #include "apl/primitives/header.h" #include "apl/utils/bimap.h" +#include "apl/utils/session.h" +#include "apl/primitives/object.h" namespace apl { @@ -52,10 +54,15 @@ struct MediaTrack { int repeatCount; // Number of times to repeat this track before moving to the next. Negative numbers repeat forever. HeaderArray headers; // HeaderArray required for the track TextTrackArray textTracks; // Distinct subtitle tracks to render + bool valid() const{ + return !url.empty(); + } }; extern Bimap sTextTrackTypeMap; +MediaTrack createMediaTrack(const Object &speech, const std::shared_ptr &context); + } // namespace apl #endif // _APL_MEDIA_TRACK_H diff --git a/aplcore/include/apl/primitives/accessibilityaction.h b/aplcore/include/apl/primitives/accessibilityaction.h index 8d6c8a7..4beec12 100644 --- a/aplcore/include/apl/primitives/accessibilityaction.h +++ b/aplcore/include/apl/primitives/accessibilityaction.h @@ -51,7 +51,14 @@ class AccessibilityAction : public ObjectData, public RecalculateTarget, public UserData { public: - friend class AccessibilityActionDependant; + /// Standard action names + static const char* ACCESSIBILITY_ACTION_ACTIVATE; + static const char* ACCESSIBILITY_ACTION_TAP; + static const char* ACCESSIBILITY_ACTION_DOUBLETAP; + static const char* ACCESSIBILITY_ACTION_LONGPRESS; + static const char* ACCESSIBILITY_ACTION_SWIPEAWAY; + static const char* ACCESSIBILITY_ACTION_SCROLLFORWARD; + static const char* ACCESSIBILITY_ACTION_SCROLLBACKWARD; /** * Create an accessibility action from an object description with "name", "label", and other properties. @@ -59,7 +66,17 @@ class AccessibilityAction : public ObjectData, * @param object A map object containing the properties that define the accessibility action. * @return The accessibility action or nullptr if the action can't be created. */ - static std::shared_ptr create(const CoreComponentPtr& component, const Object& object); + static AccessibilityActionPtr create(const CoreComponentPtr& component, const Object& object); + + /** + * Create simple accessibility action. + * @param component The component the accessibility action will be attached to. + * @param name Action name. + * @param label Action accessibility label. + * @return The accessibility action or nullptr if the action can't be created. + */ + static AccessibilityActionPtr create( + const CoreComponentPtr& component, const std::string& name, const std::string& label); /** * @return The name of the accessibility action diff --git a/aplcore/include/apl/primitives/textmeasurerequest.h b/aplcore/include/apl/primitives/textmeasurerequest.h index d1213ed..3bbd717 100644 --- a/aplcore/include/apl/primitives/textmeasurerequest.h +++ b/aplcore/include/apl/primitives/textmeasurerequest.h @@ -54,11 +54,11 @@ struct TextMeasureRequest { } bool operator==(const TextMeasureRequest& rhs) const { - return width == rhs.width && - widthMode == rhs.widthMode && - height == rhs.height && + return widthMode == rhs.widthMode && heightMode == rhs.heightMode && - paramHash == rhs.paramHash; + paramHash == rhs.paramHash && + (widthMode == YGMeasureMode::YGMeasureModeUndefined || (width == rhs.width)) && + (heightMode == YGMeasureMode::YGMeasureModeUndefined || (height == rhs.height)); } std::string toString() const { diff --git a/aplcore/include/apl/primitives/unicode.h b/aplcore/include/apl/primitives/unicode.h index 140919a..09b14e4 100644 --- a/aplcore/include/apl/primitives/unicode.h +++ b/aplcore/include/apl/primitives/unicode.h @@ -45,6 +45,14 @@ int utf8StringLength(const uint8_t* utf8StringPtr, int count); */ std::string utf8StringSlice(const std::string& utf8String, int start, int end = std::numeric_limits::max()); +/** + * Return a single character from a UTF-8 string + * @param utf8String A reference to a std::string holding UTF-8 data + * @param index The offset of the code point. If negative, count from the end of the string + * @return The extracted code point. May be empty. + */ +std::string utf8StringCharAt(const std::string& utf8String, int index); + /** * Strip invalid characters out of a UTF-8 string. The "validCharacters" property in the EditText * component defines the schema for the valid character string. diff --git a/aplcore/include/apl/scenegraph/filter.h b/aplcore/include/apl/scenegraph/filter.h index 1750d93..12e577d 100644 --- a/aplcore/include/apl/scenegraph/filter.h +++ b/aplcore/include/apl/scenegraph/filter.h @@ -26,7 +26,15 @@ namespace apl { namespace sg { -class Filter : public NonCopyable { +/** + * Filters are a tree of image transformations that are recursively applied to their children until the end filter + * is reached which can be either a MediaObjectFilter (image) or a SolidFilter (solid color or gradient). + * The Filter chain is immutable after being created, Core will not modify it. + * UserData support is provided so that viewhosts can store Filter-related data such as + * a rendered bitmap that represents the output of the Filter chain. + */ +class Filter : public NonCopyable, + public UserData { public: enum Type { kBlend, diff --git a/aplcore/include/apl/scenegraph/layer.h b/aplcore/include/apl/scenegraph/layer.h index cfd2ee2..a107bc6 100644 --- a/aplcore/include/apl/scenegraph/layer.h +++ b/aplcore/include/apl/scenegraph/layer.h @@ -77,6 +77,7 @@ class Layer : public Counter, enum Characteristics { kCharacteristicDoNotClipChildren = 1u << 0, // Do not clip children to the layer outline or clip path kCharacteristicRenderOnly = 1u << 1, // This layer is only for rendering (no text input or touch events) + kCharacteristicHasMedia = 1u << 2 // This layer contains images or videos. }; using CharacteristicsType = std::uint8_t; diff --git a/aplcore/include/apl/scenegraph/scenegraph.h b/aplcore/include/apl/scenegraph/scenegraph.h index c63d46f..b28125c 100644 --- a/aplcore/include/apl/scenegraph/scenegraph.h +++ b/aplcore/include/apl/scenegraph/scenegraph.h @@ -19,6 +19,7 @@ #include #include "apl/common.h" +#include "apl/primitives/size.h" #include "apl/scenegraph/scenegraphupdates.h" #include "apl/utils/noncopyable.h" @@ -37,6 +38,9 @@ class SceneGraph : public NonCopyable { void setLayer(const LayerPtr& layer) { mTopLayer = layer; } LayerPtr getLayer() const { return mTopLayer; } + void setViewportSize(const Size& size) { mViewportSize = size; } + Size getViewportSize() const { return mViewportSize; } + SceneGraphUpdates& updates() { return mUpdates; } rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const; @@ -44,6 +48,7 @@ class SceneGraph : public NonCopyable { private: LayerPtr mTopLayer; SceneGraphUpdates mUpdates; + Size mViewportSize; }; } // namespace sg diff --git a/aplcore/include/apl/touch/gesture.h b/aplcore/include/apl/touch/gesture.h index 7957589..85dfa8a 100644 --- a/aplcore/include/apl/touch/gesture.h +++ b/aplcore/include/apl/touch/gesture.h @@ -84,6 +84,11 @@ class Gesture : public Counter, */ virtual bool invokeAccessibilityAction(const std::string& name) { return false; } + /** + * @return Gesture-supported accessibility actions. + */ + virtual const std::vector& getAccessibilityActions() const; + protected: Gesture(const ActionablePtr& actionable) : mActionable(actionable), mStarted(false), mTriggered(false) {} diff --git a/aplcore/include/apl/touch/gestures/doublepressgesture.h b/aplcore/include/apl/touch/gestures/doublepressgesture.h index 183992f..1923bea 100644 --- a/aplcore/include/apl/touch/gestures/doublepressgesture.h +++ b/aplcore/include/apl/touch/gestures/doublepressgesture.h @@ -37,6 +37,7 @@ class DoublePressGesture : public std::enable_shared_from_this& getAccessibilityActions() const override; protected: bool onTimeUpdate(const PointerEvent& event, apl_time_t timestamp) override; diff --git a/aplcore/include/apl/touch/gestures/longpressgesture.h b/aplcore/include/apl/touch/gestures/longpressgesture.h index cdb29dd..cda1188 100644 --- a/aplcore/include/apl/touch/gestures/longpressgesture.h +++ b/aplcore/include/apl/touch/gestures/longpressgesture.h @@ -30,6 +30,7 @@ class LongPressGesture : public std::enable_shared_from_this, virtual ~LongPressGesture() = default; bool invokeAccessibilityAction(const std::string& name) override; + const std::vector& getAccessibilityActions() const override; protected: bool onTimeUpdate(const PointerEvent& event, apl_time_t timestamp) override; diff --git a/aplcore/include/apl/touch/gestures/swipeawaygesture.h b/aplcore/include/apl/touch/gestures/swipeawaygesture.h index 67b1894..333627e 100644 --- a/aplcore/include/apl/touch/gestures/swipeawaygesture.h +++ b/aplcore/include/apl/touch/gestures/swipeawaygesture.h @@ -40,10 +40,11 @@ class SwipeAwayGesture : public FlingGesture, public std::enable_shared_from_thi SwipeAwayGesture(const ActionablePtr& actionable, SwipeAwayActionType action, SwipeDirection direction, Object&& onSwipeMove, Object&& onSwipeDone, Object&& items); - virtual ~SwipeAwayGesture() = default; + ~SwipeAwayGesture() override = default; void reset() override; bool invokeAccessibilityAction(const std::string& name) override; + const std::vector& getAccessibilityActions() const override; protected: bool onMove(const PointerEvent& event, apl_time_t timestamp) override; @@ -52,7 +53,7 @@ class SwipeAwayGesture : public FlingGesture, public std::enable_shared_from_thi private: float getMove(SwipeDirection direction, Point localPos); - int getFulfillMoveDirection(); + float getFulfillMoveDirection(); std::shared_ptr getTransformation(bool above); void processTransformChange(float alpha); void animateRemainder(bool fulfilled, float velocity); diff --git a/aplcore/include/apl/touch/gestures/tapgesture.h b/aplcore/include/apl/touch/gestures/tapgesture.h index e57997d..b25b8f4 100644 --- a/aplcore/include/apl/touch/gestures/tapgesture.h +++ b/aplcore/include/apl/touch/gestures/tapgesture.h @@ -39,6 +39,7 @@ class TapGesture : public std::enable_shared_from_this, public Gestu void reset() override; bool invokeAccessibilityAction(const std::string& name) override; + const std::vector& getAccessibilityActions() const override; protected: bool onDown(const PointerEvent& event, apl_time_t timestamp) override; diff --git a/aplcore/include/apl/utils/session.h b/aplcore/include/apl/utils/session.h index e673c89..bebd786 100644 --- a/aplcore/include/apl/utils/session.h +++ b/aplcore/include/apl/utils/session.h @@ -17,10 +17,21 @@ #define _APL_SESSION_H #include "apl/common.h" +#include "apl/primitives/object.h" #include "apl/utils/log.h" namespace apl { +/** + * A message generated by the Log command, originating from the APL document author. + */ +struct LogCommandMessage { + std::string text; // Command-specified message text + LogLevel level; // Command-specified log level + Object arguments; // Command-specified additional arguments + Object origin; // Event handler source, for tracking provenance +}; + /** * The Session object provides a virtual console to report errors and problems that occur when * parsing the APL document and packages. These are the errors that should be reported back to @@ -62,6 +73,19 @@ class Session { write(filename, func, value.c_str()); } + /** + * Report a log message resulting from execution of the Log command. The view host is + * responsible for reporting this information to the APL document author when a valid debugging + * context is available. When a valid debugging context is not available, the entry should be + * discarded, since it could contain arbitrary customer information from the APL document's + * execution context. + * + * By default, the log message is discarded. + * + * @param message The log command message payload + */ + virtual void write(LogCommandMessage&& message) {} + /** * Set log prefix to be used. * @param prefix [A-Z], truncated to 6 characters. Padded with '_' diff --git a/aplcore/src/action/animatedscrollaction.cpp b/aplcore/src/action/animatedscrollaction.cpp index 1337584..29d8b77 100644 --- a/aplcore/src/action/animatedscrollaction.cpp +++ b/aplcore/src/action/animatedscrollaction.cpp @@ -31,7 +31,9 @@ AnimatedScrollAction::AnimatedScrollAction(const TimersPtr& timers, mDuration(duration) { // Default to programmatic duration if not specified - mDuration = mDuration >= 0 ? mDuration : context->getRootConfig().getScrollCommandDuration(); + mDuration = mDuration >= 0 + ? mDuration + : context->getRootConfig().getProperty(RootProperty::kScrollCommandDuration).getDouble(); } void diff --git a/aplcore/src/action/arrayaction.cpp b/aplcore/src/action/arrayaction.cpp index 57495ea..2667e30 100644 --- a/aplcore/src/action/arrayaction.cpp +++ b/aplcore/src/action/arrayaction.cpp @@ -22,10 +22,12 @@ namespace apl { -ArrayAction::ArrayAction(const TimersPtr& timers, std::shared_ptr&& command, bool fastMode) +ArrayAction::ArrayAction(const TimersPtr& timers, const ContextPtr& context, std::shared_ptr&& command, CommandData&& data, bool fastMode) : Action(timers), mCommand(std::move(command)), mFastMode(fastMode), + mContext(context), + mData(std::move(data)), mNextIndex(0) { addTerminateCallback([this](const TimersPtr&) { @@ -35,13 +37,16 @@ ArrayAction::ArrayAction(const TimersPtr& timers, std::shared_ptrfinishAllOnTerminate()) { - const auto& commands = mCommand->data().get(); + const auto& commands = mData.get(); std::vector remaining; for (size_t i = mNextIndex ; i < commands.size() ; i++) remaining.push_back(commands.at(i)); - auto context = mCommand->context(); - context->sequencer().executeCommands({std::move(remaining), mCommand->data()}, context, mCommand->base(), true); + mContext->sequencer().executeCommands( + {std::move(remaining), mData}, + mContext, + mCommand->base(), + true); } }); } @@ -55,16 +60,14 @@ ArrayAction::advance() { if (isTerminated()) return; - const auto& commands = mCommand->data(); - - while (mNextIndex < commands.size()) { - auto commandPtr = CommandFactory::instance().inflate(commands.at(mNextIndex++), mCommand); + while (mNextIndex < mData.size()) { + auto commandPtr = CommandFactory::instance().inflate(mContext, mData.at(mNextIndex++), mCommand); if (!commandPtr) continue; auto childSeq = commandPtr->sequencer(); if (childSeq != mCommand->sequencer()) { - mCommand->context()->sequencer().executeOnSequencer(commandPtr, childSeq); + mContext->sequencer().executeOnSequencer(commandPtr, childSeq); continue; } diff --git a/aplcore/src/action/autopageaction.cpp b/aplcore/src/action/autopageaction.cpp index eab54ee..3850b9f 100644 --- a/aplcore/src/action/autopageaction.cpp +++ b/aplcore/src/action/autopageaction.cpp @@ -27,13 +27,15 @@ AutoPageAction::AutoPageAction(const TimersPtr& timers, const ComponentPtr& container, int start, int end, - apl_time_t duration) + apl_duration_t duration, + apl_duration_t transitionDuration) : ResourceHoldingAction(timers, command->context()), mCommand(command), mContainer(container), mNextIndex(start), mEndIndex(end), - mDuration(duration) + mDuration(duration), + mTransitionDuration(transitionDuration) { addTerminateCallback([this](const TimersPtr&) { if (mCurrentAction) { @@ -58,13 +60,14 @@ AutoPageAction::make(const TimersPtr& timers, auto len = static_cast(target->getChildCount()); auto count = command->getValue(kCommandPropertyCount).asInt(); auto duration = command->getValue(kCommandPropertyDuration).asInt(); + auto transitionDuration = command->getValue(kCommandPropertyTransitionDuration).asInt(); if (count <= 0 || start >= len) return nullptr; count = std::min(len - start, count); // Note: Count may be INT_MAX, so be careful with math - auto ptr = std::make_shared(timers, command, target, start, start + count, duration); + auto ptr = std::make_shared(timers, command, target, start, start + count, duration, transitionDuration); command->context()->sequencer().claimResource({kExecutionResourcePosition, target}, ptr); ptr->advance(); return ptr; @@ -89,8 +92,9 @@ AutoPageAction::advance() { bool firstTime = (mCurrentAction == nullptr); mCurrentAction = Action::makeDelayed(timers(), (firstTime ? 0 : mDuration), [this](ActionRef ref) { - PagerComponent::setPageUtil(mContext, mContainer, mNextIndex++, kPageDirectionForward, ref, - mContext->getRequestedAPLVersion().compare("1.6") < 0); + PagerComponent::setPageUtil(mContainer, mNextIndex++, kPageDirectionForward, ref, + mContext->getRequestedAPLVersion().compare("1.6") < 0, + mTransitionDuration); }); } diff --git a/aplcore/src/action/scrollaction.cpp b/aplcore/src/action/scrollaction.cpp index f840758..fb8ff2d 100644 --- a/aplcore/src/action/scrollaction.cpp +++ b/aplcore/src/action/scrollaction.cpp @@ -31,9 +31,10 @@ ScrollAction::ScrollAction(const TimersPtr& timers, std::shared_ptr ScrollAction::make(const TimersPtr& timers, - const std::shared_ptr& command) + const std::shared_ptr& command, + apl_duration_t duration) { - return make(timers, command->context(), command->target(), command->getValue(kCommandPropertyDistance)); + return make(timers, command->context(), command->target(), command->getValue(kCommandPropertyDistance), duration); } std::shared_ptr diff --git a/aplcore/src/action/scrolltoaction.cpp b/aplcore/src/action/scrolltoaction.cpp index cd04825..ed0be73 100644 --- a/aplcore/src/action/scrolltoaction.cpp +++ b/aplcore/src/action/scrolltoaction.cpp @@ -54,13 +54,14 @@ ScrollToAction::make(const TimersPtr& timers, std::shared_ptr ScrollToAction::make(const TimersPtr& timers, const std::shared_ptr& command, - const CoreComponentPtr& target) + const CoreComponentPtr& target, + apl_duration_t duration) { auto t = target ? target : command->target(); if (!t) return nullptr; auto align = static_cast(command->getValue(kCommandPropertyAlign).getInteger()); - return make(timers, align, Rect(), command->context(), false, t); + return make(timers, align, Rect(), command->context(), false, t, duration); } std::shared_ptr @@ -141,7 +142,9 @@ ScrollToAction::make(const TimersPtr& timers, scrollToSubBounds, target, CoreComponent::cast(container), - duration >= 0 ? duration : context->getRootConfig().getScrollCommandDuration()); + duration >= 0 + ? duration + : context->getRootConfig().getProperty(RootProperty::kScrollCommandDuration).getDouble()); context->sequencer().claimResource({kExecutionResourcePosition, container}, ptr); @@ -296,7 +299,7 @@ ScrollToAction::pageTo() // We assume we were invoked from a ScrollToComponent/Index command. We use absolute // positioning. - PagerComponent::setPageUtil(mContext, mContainer, targetPage, + PagerComponent::setPageUtil(mContainer, targetPage, targetPage < currentPage ? kPageDirectionBack : kPageDirectionForward, shared_from_this(), mContext->getRequestedAPLVersion().compare("1.6") < 0); } diff --git a/aplcore/src/action/sequentialaction.cpp b/aplcore/src/action/sequentialaction.cpp index 4d3cb90..7e5e7f2 100644 --- a/aplcore/src/action/sequentialaction.cpp +++ b/aplcore/src/action/sequentialaction.cpp @@ -15,6 +15,7 @@ #include "apl/action/sequentialaction.h" +#include "apl/action/arrayaction.h" #include "apl/action/delayaction.h" #include "apl/command/commandfactory.h" #include "apl/time/sequencer.h" @@ -84,10 +85,45 @@ SequentialAction::advance() { auto repeatCount = mCommand->getValue(kCommandPropertyRepeatCount).asInt(); while (mRepeatCounter <= repeatCount) { - while (mNextIndex < commands.size()) { - const auto& command = commands.at(mNextIndex++); - if (doCommand({command, mCommand->data()})) - return; // Done advancing until the current action resolves + auto data = mCommand->getValue(kCommandPropertyData); + + // If there is no data, proceed through commands list + if (data.empty()) { + while (mNextIndex < commands.size()) { + const auto& command = commands.at(mNextIndex++); + if (doCommand({command, mCommand->data()})) + return; // Done advancing until the current action resolves + } + } + else { + while (mNextIndex < data.size()) { + auto dataLength = data.size(); + auto index = mNextIndex++; + const auto& datum = data.at(index); + auto childContext = Context::createFromParent(mCommand->context()); + childContext->putConstant("data", datum); + childContext->putConstant("index", index); + childContext->putConstant("length", dataLength); + + mCurrentAction = ArrayAction::make(timers(), childContext, mCommand, + CommandData(commands), mFastMode); + + if (!mCurrentAction) + continue; + + std::weak_ptr weak_ptr( + std::static_pointer_cast(shared_from_this())); + mCurrentAction->then([weak_ptr](const ActionPtr&) { + auto self = weak_ptr.lock(); + if (self) { + self->mCurrentAction = nullptr; + if (!self->isTerminated()) + self->advance(); + } + }); + + return; + } } mRepeatCounter++; mNextIndex = 0; diff --git a/aplcore/src/action/setpageaction.cpp b/aplcore/src/action/setpageaction.cpp index f394ae0..27e2090 100644 --- a/aplcore/src/action/setpageaction.cpp +++ b/aplcore/src/action/setpageaction.cpp @@ -36,10 +36,12 @@ inline int modulus(int a, int b) SetPageAction::SetPageAction(const TimersPtr& timers, const std::shared_ptr& command, - const CoreComponentPtr& target) + const CoreComponentPtr& target, + apl_duration_t transitionDuration) : ResourceHoldingAction(timers, command->context()), mCommand(command), - mTarget(target) + mTarget(target), + mTransitionDuration(transitionDuration) {} std::shared_ptr @@ -53,7 +55,8 @@ SetPageAction::make(const TimersPtr& timers, || target->getChildCount() < 2) return nullptr; - auto ptr = std::make_shared(timers, command, target); + auto transitionDuration = command->getValue(kCommandPropertyTransitionDuration).getInteger(); + auto ptr = std::make_shared(timers, command, target, transitionDuration); command->context()->sequencer().claimResource({kExecutionResourcePosition, target}, ptr); ptr->start(); return ptr; @@ -101,8 +104,9 @@ SetPageAction::start() } else { mTarget->ensureChildLayout(mTarget->getCoreChildAt(mTargetIndex), true); - PagerComponent::setPageUtil(mContext, mTarget, mTargetIndex, direction, shared_from_this(), - position == kCommandPositionAbsolute || mContext->getRequestedAPLVersion().compare("1.6") < 0); + PagerComponent::setPageUtil(mTarget, mTargetIndex, direction, shared_from_this(), + position == kCommandPositionAbsolute || mContext->getRequestedAPLVersion().compare("1.6") < 0, + mTransitionDuration); } } diff --git a/aplcore/src/action/speakitemaction.cpp b/aplcore/src/action/speakitemaction.cpp index 85553bb..f7bec6a 100644 --- a/aplcore/src/action/speakitemaction.cpp +++ b/aplcore/src/action/speakitemaction.cpp @@ -24,6 +24,8 @@ #include "apl/time/sequencer.h" #include "apl/utils/make_unique.h" #include "apl/utils/principal_ptr.h" +#include "apl/utils/session.h" +#include "apl/media/mediatrack.h" #ifdef SCENEGRAPH #include "apl/scenegraph/textlayout.h" @@ -75,6 +77,16 @@ class SpeakItemActionPrivate { virtual void start(SpeakItemAction& action) { auto context = action.mCommand->context(); + // Create a MediaTrack from Speech Property + MediaTrack track = createMediaTrack(action.mTarget->getCalculated(kPropertySpeech), context); + if(!track.valid()){ + CONSOLE(context).log("Audio source missing in playback"); + return; + } + action.mSource = track.url; + LOG_IF(DEBUG_SPEAK_ITEM) << "source: " << action.mSource + << ", lineMode: " << !mText.empty(); + // If we are doing line highlighting, grab a copy of the text in the component if (action.mTarget->getType() == kComponentTypeText && action.mCommand->getValue(kCommandPropertyHighlightMode) == kCommandHighlightModeLine) { @@ -84,11 +96,6 @@ class SpeakItemActionPrivate { } // Create an audio player and queue up the TTS as the track - action.mSource = action.mTarget->getCalculated(kPropertySpeech).asString(); - - LOG_IF(DEBUG_SPEAK_ITEM) << "source: " << action.mSource - << ", lineMode: " << !mText.empty(); - const auto& factory = context->getRootConfig().getAudioPlayerFactory(); std::weak_ptr weak_ptr( std::static_pointer_cast(action.shared_from_this())); @@ -142,12 +149,7 @@ class SpeakItemActionPrivate { // Effectively preroll if (mAudioPlayer) { - mAudioPlayer->setTrack(MediaTrack{ - action.mSource, // URL - 0, // Start - 0, // Duration (play the entire track) - 0, // Repeat count - }); + mAudioPlayer->setTrack(track); } } } diff --git a/aplcore/src/command/CMakeLists.txt b/aplcore/src/command/CMakeLists.txt index 7968e77..b9d1f57 100644 --- a/aplcore/src/command/CMakeLists.txt +++ b/aplcore/src/command/CMakeLists.txt @@ -27,6 +27,7 @@ target_sources_local(apl extensioneventcommand.cpp finishcommand.cpp insertitemcommand.cpp + logcommand.cpp openurlcommand.cpp parallelcommand.cpp playmediacommand.cpp diff --git a/aplcore/src/command/autopagecommand.cpp b/aplcore/src/command/autopagecommand.cpp index 4c1a585..d64a0a2 100644 --- a/aplcore/src/command/autopagecommand.cpp +++ b/aplcore/src/command/autopagecommand.cpp @@ -22,9 +22,10 @@ namespace apl { const CommandPropDefSet& AutoPageCommand::propDefSet() const { static CommandPropDefSet sAutoPageCommandProperties( CoreCommand::propDefSet(), { - {kCommandPropertyComponentId, "", asString, kPropRequiredId}, - {kCommandPropertyCount, std::numeric_limits::max(), asNonNegativeInteger }, - {kCommandPropertyDuration, 0, asNonNegativeInteger } + {kCommandPropertyComponentId, "", asString, kPropRequiredId}, + {kCommandPropertyCount, std::numeric_limits::max(), asNonNegativeInteger }, + {kCommandPropertyDuration, 0, asNonNegativeInteger }, + {kCommandPropertyTransitionDuration, -1, asInteger}, }); return sAutoPageCommandProperties; diff --git a/aplcore/src/command/commandfactory.cpp b/aplcore/src/command/commandfactory.cpp index 381a9bf..1dffd77 100644 --- a/aplcore/src/command/commandfactory.cpp +++ b/aplcore/src/command/commandfactory.cpp @@ -168,6 +168,14 @@ CommandFactory::inflate(const ContextPtr& context, return inflate(context, std::move(commandData), Properties(), base); } +CommandPtr +CommandFactory::inflate(const ContextPtr& context, + CommandData&& commandData, + const std::shared_ptr& parent) +{ + return inflate(context, std::move(commandData), Properties(), parent->base(), parent->sequencer()); +} + CommandPtr CommandFactory::inflate(CommandData&& commandData, const std::shared_ptr& parent) { diff --git a/aplcore/src/command/commandproperties.cpp b/aplcore/src/command/commandproperties.cpp index 1ba1ee6..478dff6 100644 --- a/aplcore/src/command/commandproperties.cpp +++ b/aplcore/src/command/commandproperties.cpp @@ -43,6 +43,7 @@ Bimap sCommandNameBimap = { {kCommandTypeReinflate, "Reinflate"}, {kCommandTypeInsertItem, "InsertItem"}, {kCommandTypeRemoveItem, "RemoveItem"}, + {kCommandTypeLog, "Log"}, }; Bimap sCommandPropertyBimap = { @@ -68,6 +69,8 @@ Bimap sCommandPropertyBimap = { {kCommandPropertyIndex, "index"}, {kCommandPropertyItem, "item"}, {kCommandPropertyItem, "items"}, + {kCommandPropertyLevel, "level"}, + {kCommandPropertyMessage, "message"}, {kCommandPropertyMinimumDwellTime, "minimumDwellTime"}, {kCommandPropertyOnFail, "onFail"}, {kCommandPropertyOtherwise, "otherwise"}, @@ -82,6 +85,8 @@ Bimap sCommandPropertyBimap = { {kCommandPropertySource, "source"}, {kCommandPropertyStart, "start"}, {kCommandPropertyState, "state"}, + {kCommandPropertyTargetDuration, "targetDuration"}, + {kCommandPropertyTransitionDuration, "transitionDuration"}, {kCommandPropertyValue, "value"}, {kCommandPropertyValue, "values"}, }; @@ -131,4 +136,12 @@ Bimap sCommandReasonMap = { {kCommandReasonExit, "exit"} }; -} // namespace apl \ No newline at end of file +Bimap sCommandLogLevelMap = { + {kCommandLogLevelDebug, "debug"}, + {kCommandLogLevelInfo, "info"}, + {kCommandLogLevelWarn, "warn"}, + {kCommandLogLevelError, "error"}, + {kCommandLogLevelCritical, "critical"} +}; + +} // namespace apl diff --git a/aplcore/src/command/corecommand.cpp b/aplcore/src/command/corecommand.cpp index 0225bd0..ec453f4 100644 --- a/aplcore/src/command/corecommand.cpp +++ b/aplcore/src/command/corecommand.cpp @@ -21,6 +21,7 @@ #include "apl/command/autopagecommand.h" #include "apl/command/clearfocuscommand.h" #include "apl/command/controlmediacommand.h" +#include "apl/command/logcommand.h" #include "apl/command/finishcommand.h" #include "apl/command/idlecommand.h" #include "apl/command/insertitemcommand.h" @@ -253,7 +254,15 @@ CoreCommand::calculateProperties() } } - if (mTarget != nullptr) mValues.emplace(kCommandPropertyComponentId, mTarget->getUniqueId()); + if (mTarget != nullptr) { + // Can't target a disallowed component + if (mTarget->isDisallowed()) { + CONSOLE(mContext) << "Component type " << mTarget->name() << " is disallowed, ignoring command " << name(); + return false; + } + + mValues.emplace(kCommandPropertyComponentId, mTarget->getUniqueId()); + } } // When we have a target component, we need to update the context "event" property to include @@ -302,6 +311,7 @@ std::map sCommandCreatorMap = { {kCommandTypeAutoPage, AutoPageCommand::create}, {kCommandTypeControlMedia, ControlMediaCommand::create}, {kCommandTypeIdle, IdleCommand::create}, + {kCommandTypeLog, LogCommand::create}, {kCommandTypeOpenURL, OpenURLCommand::create}, {kCommandTypeParallel, ParallelCommand::create}, {kCommandTypePlayMedia, PlayMediaCommand::create}, diff --git a/aplcore/src/command/insertitemcommand.cpp b/aplcore/src/command/insertitemcommand.cpp index 20d611f..22d3018 100644 --- a/aplcore/src/command/insertitemcommand.cpp +++ b/aplcore/src/command/insertitemcommand.cpp @@ -41,6 +41,16 @@ InsertItemCommand::propDefSet() const { return sInsertItemCommandProperties; } +ContextPtr +InsertItemCommand::buildBaseChildContext(int insertIndex) const +{ + auto length = target()->getChildCount() + 1; + auto childContext = Context::createFromParent(target()->getContext()); + childContext->putSystemWriteable("index", insertIndex); + childContext->putSystemWriteable("length", length); + return childContext; +} + ActionPtr InsertItemCommand::execute(const TimersPtr& timers, bool fastMode) { @@ -51,13 +61,29 @@ InsertItemCommand::execute(const TimersPtr& timers, bool fastMode) { (int) target()->getChildCount(), getValue(kCommandPropertyAt).asInt()); - auto child = Builder().inflate( - target()->getContext(), - getValue(kCommandPropertyItem)); + auto childContext = target()->multiChild() ? + buildBaseChildContext(index) : + Context::createFromParent(target()->getContext()); + auto child = Builder(nullptr) + .expandSingleComponentFromArray(childContext, + arrayify(*childContext, getValue(kCommandPropertyItem)), + Properties(), + target(), + target()->getPathObject().addIndex(index), + // Force to inflate component's children as rebuilder + // not involved and nothing will be able to inflate + // lazily. + true, + true); if (!child || !child->isValid()) CONSOLE(mContext) << "Could not inflate item to be inserted"; - else if (!target()->insertChild(child, index)) + else if (target()->insertChild(child, index)) { + // Allow lazy components to process new children layout (if any). + target()->processLayoutChanges(true, false); + // And allow for full DOM to adjust any changed relative sizes + CoreComponent::cast(mContext->topComponent())->processLayoutChanges(true, false); + } else CONSOLE(mContext) << "Could not insert child into '" << target()->getId() << "'"; return nullptr; diff --git a/aplcore/src/command/logcommand.cpp b/aplcore/src/command/logcommand.cpp new file mode 100644 index 0000000..e5e294e --- /dev/null +++ b/aplcore/src/command/logcommand.cpp @@ -0,0 +1,107 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "apl/command/logcommand.h" +#include "apl/utils/session.h" + +namespace apl { + +const static bool DEBUG_LOG_COMMAND = false; + +const CommandPropDefSet& +LogCommand::propDefSet() const +{ + static CommandPropDefSet sLogCommandProperties(CoreCommand::propDefSet(), { + {kCommandPropertyLevel, kCommandLogLevelInfo, asAny}, + {kCommandPropertyMessage, "", asString}, + {kCommandPropertyArguments, Object::EMPTY_ARRAY(), asArray}, + }); + return sLogCommandProperties; +} + +ActionPtr +LogCommand::execute(const TimersPtr& timers, bool fastMode) { + if (!mContext || !calculateProperties()) + return nullptr; + + // Level can be specified as a number or a string. + int levelValue = kCommandLogLevelInfo; + auto level = getValue(kCommandPropertyLevel); + if (level.isNumber()) { + auto levelAsInt = level.asInt(); + levelValue = sCommandLogLevelMap.has(levelAsInt) ? levelAsInt : levelValue; + } else { + levelValue = sCommandLogLevelMap.get(level.asString(), levelValue); + } + + // Translate command property to logger log level + LogLevel logLevel = LogLevel::kInfo; + switch (levelValue) { + case kCommandLogLevelDebug: + logLevel = LogLevel::kDebug; + break; + case kCommandLogLevelInfo: + logLevel = LogLevel::kInfo; + break; + case kCommandLogLevelWarn: + logLevel = LogLevel::kWarn; + break; + case kCommandLogLevelError: + logLevel = LogLevel::kError; + break; + case kCommandLogLevelCritical: + logLevel = LogLevel::kCritical; + break; + default: + // If we add a command level property and forget to add a case above, we end up here + assert(false); + } + + // Freeze the arguments array as a JSON object + mArguments = mValues.at(kCommandPropertyArguments).serialize(mDocument.GetAllocator()); + rapidjson::Document argumentsDoc; + argumentsDoc.CopyFrom(mArguments, argumentsDoc.GetAllocator()); + + // Freeze the "event.source" property as a JSON object + auto event = mContext->opt("event"); + if (event.empty()) { + LOG(LogLevel::kError) + << "Event field not available in context. Should not happen during normal operation."; + return nullptr; + } + mSource = event.get("source").serialize(mDocument.GetAllocator()); + rapidjson::Document sourceDoc; + sourceDoc.CopyFrom(mSource, sourceDoc.GetAllocator()); + + LogCommandMessage message{ + getValue(kCommandPropertyMessage).asString(), + logLevel, + Object(std::move(argumentsDoc)), + Object(std::move(sourceDoc)) + }; + + if (DEBUG_LOG_COMMAND) { + LOG(LogLevel::kDebug).session(mContext) << "Log command: " << message.text + << ", level=" << sCommandLogLevelMap.at(levelValue) + << ", arguments=" << message.arguments.toDebugString() + << ", origin=" << message.origin.toDebugString(); + } + + mContext->session()->write(std::move(message)); + + return nullptr; +} + +} // namespace apl diff --git a/aplcore/src/command/openurlcommand.cpp b/aplcore/src/command/openurlcommand.cpp index 4947224..2f9c26f 100644 --- a/aplcore/src/command/openurlcommand.cpp +++ b/aplcore/src/command/openurlcommand.cpp @@ -41,7 +41,7 @@ OpenURLCommand::execute(const TimersPtr& timers, bool fastMode) { return nullptr; auto self = std::static_pointer_cast(shared_from_this()); - auto allowed = mContext->getRootConfig().getAllowOpenUrl(); + auto allowed = mContext->getRootConfig().getProperty(RootProperty::kAllowOpenUrl).getBoolean(); if(!allowed) { // 405 - HTTP code for web "Method Not Allowed". return OpenURLAction::makeFailed(timers, self, 405); diff --git a/aplcore/src/command/parallelcommand.cpp b/aplcore/src/command/parallelcommand.cpp index 002bd06..f0d73ae 100644 --- a/aplcore/src/command/parallelcommand.cpp +++ b/aplcore/src/command/parallelcommand.cpp @@ -14,8 +14,9 @@ */ #include "apl/command/parallelcommand.h" -#include "apl/command/commandfactory.h" +#include "apl/action/arrayaction.h" #include "apl/action/delayaction.h" +#include "apl/command/commandfactory.h" #include "apl/time/sequencer.h" namespace apl { @@ -24,7 +25,8 @@ const CommandPropDefSet& ParallelCommand::propDefSet() const { static CommandPropDefSet sParallelCommandProperties(CoreCommand::propDefSet(), { - {kCommandPropertyCommands, Object::EMPTY_ARRAY(), asArray, kPropRequired} + {kCommandPropertyCommands, Object::EMPTY_ARRAY(), asArray, kPropRequired}, + {kCommandPropertyData, Object::EMPTY_ARRAY(), asArray }, }); return sParallelCommandProperties; } @@ -34,21 +36,46 @@ ParallelCommand::execute(const TimersPtr& timers, bool fastMode) { if (!calculateProperties()) return nullptr; - auto commands = mValues.at(kCommandPropertyCommands); ActionList actions; - for (auto& command : commands.getArray()) { + auto commands = mValues.at(kCommandPropertyCommands); + if (!commands.empty()) { + auto data = mValues.at(kCommandPropertyData); auto self = std::static_pointer_cast(shared_from_this()); - auto commandPtr = CommandFactory::instance().inflate({command, data()}, self); - if (commandPtr) { - auto childSeq = commandPtr->sequencer(); - if (childSeq != sequencer()) { - context()->sequencer().executeOnSequencer(commandPtr, childSeq); - continue; + + // If there is no data, proceed through commands list + if (data.empty()) { + for (auto& command : commands.getArray()) { + auto commandPtr = CommandFactory::instance().inflate({command, this->data()}, self); + if (commandPtr) { + auto childSeq = commandPtr->sequencer(); + if (childSeq != sequencer()) { + context()->sequencer().executeOnSequencer(commandPtr, childSeq); + continue; + } + auto action = DelayAction::make(timers, commandPtr, fastMode); + if (action) + actions.push_back(action); + } + } + } else { // Iterate through the data items, and assemble a sequence of commands for each + // data element + int index = 0; + auto dataLength = data.size(); + for (const auto& datum : data.getArray()) { + auto childContext = Context::createFromParent(context()); + childContext->putConstant("data", datum); + childContext->putConstant("index", index); + childContext->putConstant("length", dataLength); + + + auto shared = std::static_pointer_cast(shared_from_this()); + auto action = ArrayAction::make(timers, childContext, shared, CommandData(commands), fastMode); + if (action) + actions.push_back(action); + + index++; } - auto action = DelayAction::make(timers, commandPtr, fastMode); - if (action) - actions.push_back(action); } } diff --git a/aplcore/src/command/reinflatecommand.cpp b/aplcore/src/command/reinflatecommand.cpp index 1256f4d..00492cd 100644 --- a/aplcore/src/command/reinflatecommand.cpp +++ b/aplcore/src/command/reinflatecommand.cpp @@ -52,6 +52,9 @@ ReinflateCommand::execute(const TimersPtr& timers, bool fastMode) return nullptr; } + if (CoreDocumentContext::cast(mContext->documentContext())->refreshContent()) + LOG(LogLevel::kDebug) << "Content re-resolution required after reinflate"; + // Return a simple action that pushes the event and does nothing else. The view host must // resolve this event to allow further events in the sequencer to execute. return Action::make(timers, [this](ActionRef ref) { diff --git a/aplcore/src/command/scrollcommand.cpp b/aplcore/src/command/scrollcommand.cpp index ba4d487..7fe7c1e 100644 --- a/aplcore/src/command/scrollcommand.cpp +++ b/aplcore/src/command/scrollcommand.cpp @@ -22,8 +22,9 @@ namespace apl { const CommandPropDefSet& ScrollCommand::propDefSet() const { static CommandPropDefSet sScrollCommandProperties(CoreCommand::propDefSet(), { - {kCommandPropertyComponentId, "", asString, kPropRequiredId}, - {kCommandPropertyDistance, 0, asNonAutoRelativeDimension}, + {kCommandPropertyComponentId, "", asString, kPropRequiredId}, + {kCommandPropertyDistance, 0, asNonAutoRelativeDimension}, + {kCommandPropertyTargetDuration, -1, asInteger}, }); return sScrollCommandProperties; @@ -44,7 +45,10 @@ ScrollCommand::execute(const TimersPtr& timers, bool fastMode) { return nullptr; } - return ScrollAction::make(timers, std::static_pointer_cast(shared_from_this())); + return ScrollAction::make( + timers, + std::static_pointer_cast(shared_from_this()), + getValue(kCommandPropertyTargetDuration).getInteger()); } } // namespace apl \ No newline at end of file diff --git a/aplcore/src/command/scrolltocomponentcommand.cpp b/aplcore/src/command/scrolltocomponentcommand.cpp index caa1c09..9efe12c 100644 --- a/aplcore/src/command/scrolltocomponentcommand.cpp +++ b/aplcore/src/command/scrolltocomponentcommand.cpp @@ -22,8 +22,9 @@ namespace apl { const CommandPropDefSet& ScrollToComponentCommand::propDefSet() const { static CommandPropDefSet sScrollToComponentCommandProperties(CoreCommand::propDefSet(), { - {kCommandPropertyAlign, kCommandScrollAlignVisible, sCommandAlignMap }, - {kCommandPropertyComponentId, "", asString, kPropRequiredId} + {kCommandPropertyAlign, kCommandScrollAlignVisible, sCommandAlignMap }, + {kCommandPropertyComponentId, "", asString, kPropRequiredId}, + {kCommandPropertyTargetDuration, -1, asInteger}, }); return sScrollToComponentCommandProperties; } @@ -38,7 +39,11 @@ ScrollToComponentCommand::execute(const TimersPtr& timers, bool fastMode) { if (!calculateProperties()) return nullptr; - return ScrollToAction::make(timers, std::static_pointer_cast(shared_from_this())); + return ScrollToAction::make( + timers, + std::static_pointer_cast(shared_from_this()), + target(), + getValue(kCommandPropertyTargetDuration).getInteger()); } } // namespace apl \ No newline at end of file diff --git a/aplcore/src/command/scrolltoindexcommand.cpp b/aplcore/src/command/scrolltoindexcommand.cpp index ddcc1b1..d0f3d97 100644 --- a/aplcore/src/command/scrolltoindexcommand.cpp +++ b/aplcore/src/command/scrolltoindexcommand.cpp @@ -22,9 +22,10 @@ namespace apl { const CommandPropDefSet& ScrollToIndexCommand::propDefSet() const { static CommandPropDefSet sScrollToIndexCommandProperties(CoreCommand::propDefSet(), { - {kCommandPropertyAlign, kCommandScrollAlignVisible, sCommandAlignMap }, - {kCommandPropertyComponentId, "", asString, kPropRequiredId}, - {kCommandPropertyIndex, 0, asInteger, kPropRequired}, + {kCommandPropertyAlign, kCommandScrollAlignVisible, sCommandAlignMap }, + {kCommandPropertyComponentId, "", asString, kPropRequiredId}, + {kCommandPropertyIndex, 0, asInteger, kPropRequired}, + {kCommandPropertyTargetDuration, -1, asInteger}, }); return sScrollToIndexCommandProperties; @@ -51,7 +52,11 @@ ScrollToIndexCommand::execute(const TimersPtr& timers, bool fastMode) { mTarget = mTarget->getCoreChildAt(childIndex); - return ScrollToAction::make(timers, std::static_pointer_cast(shared_from_this())); + return ScrollToAction::make( + timers, + std::static_pointer_cast(shared_from_this()), + target(), + getValue(kCommandPropertyTargetDuration).getInteger()); } } // namespace apl \ No newline at end of file diff --git a/aplcore/src/command/sequentialcommand.cpp b/aplcore/src/command/sequentialcommand.cpp index 9b7423b..b62786f 100644 --- a/aplcore/src/command/sequentialcommand.cpp +++ b/aplcore/src/command/sequentialcommand.cpp @@ -22,6 +22,7 @@ const CommandPropDefSet& SequentialCommand::propDefSet() const { static CommandPropDefSet sSequentialCommandProperties(CoreCommand::propDefSet(), { {kCommandPropertyCommands, Object::EMPTY_ARRAY(), asArray }, + {kCommandPropertyData, Object::EMPTY_ARRAY(), asArray }, {kCommandPropertyRepeatCount, 0, asNonNegativeInteger }, {kCommandPropertyCatch, Object::EMPTY_ARRAY(), asArray }, {kCommandPropertyFinally, Object::EMPTY_ARRAY(), asArray } diff --git a/aplcore/src/command/setpagecommand.cpp b/aplcore/src/command/setpagecommand.cpp index e489b72..21564ed 100644 --- a/aplcore/src/command/setpagecommand.cpp +++ b/aplcore/src/command/setpagecommand.cpp @@ -22,9 +22,10 @@ namespace apl { const CommandPropDefSet& SetPageCommand::propDefSet() const { static CommandPropDefSet sSetPageCommandProperties(CoreCommand::propDefSet(), { - {kCommandPropertyComponentId, "", asString, kPropRequiredId}, - {kCommandPropertyPosition, kCommandPositionAbsolute, sCommandPositionMap }, - {kCommandPropertyValue, 0, asInteger, kPropRequired} + {kCommandPropertyComponentId, "", asString, kPropRequiredId}, + {kCommandPropertyPosition, kCommandPositionAbsolute, sCommandPositionMap }, + {kCommandPropertyTransitionDuration, -1, asInteger}, + {kCommandPropertyValue, 0, asInteger, kPropRequired} }); return sSetPageCommandProperties; diff --git a/aplcore/src/component/actionablecomponent.cpp b/aplcore/src/component/actionablecomponent.cpp index 45e11d0..0b53148 100644 --- a/aplcore/src/component/actionablecomponent.cpp +++ b/aplcore/src/component/actionablecomponent.cpp @@ -46,7 +46,7 @@ ActionableComponent::propDefSet() const { } std::shared_ptr -ActionableComponent::cast(const std::shared_ptr& component) { +ActionableComponent::cast(const ComponentPtr& component) { return component && CoreComponent::cast(component)->isActionable() ? std::static_pointer_cast(component) : nullptr; } @@ -170,6 +170,18 @@ ActionableComponent::processGestures(const PointerEvent& event, apl_time_t times return false; } +void +ActionableComponent::getSupportedStandardAccessibilityActions(std::map& result) const +{ + if (getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureDynamicAccessibilityActions)) { + for (const auto& ga : mGestureHandlers) { + for (const auto& aa : ga->getAccessibilityActions()) { + result.emplace(aa, false); + } + } + } +} + void ActionableComponent::invokeStandardAccessibilityAction(const std::string& name) { diff --git a/aplcore/src/component/componenteventsourcewrapper.cpp b/aplcore/src/component/componenteventsourcewrapper.cpp index d4b325f..77a3cbc 100644 --- a/aplcore/src/component/componenteventsourcewrapper.cpp +++ b/aplcore/src/component/componenteventsourcewrapper.cpp @@ -19,7 +19,7 @@ namespace apl { std::shared_ptr -ComponentEventSourceWrapper::create(const std::shared_ptr &component, +ComponentEventSourceWrapper::create(const ConstCoreComponentPtr &component, std::string handler, const Object &value) { auto result = std::make_shared(component); diff --git a/aplcore/src/component/componenteventtargetwrapper.cpp b/aplcore/src/component/componenteventtargetwrapper.cpp index 706ecd5..e70da54 100644 --- a/aplcore/src/component/componenteventtargetwrapper.cpp +++ b/aplcore/src/component/componenteventtargetwrapper.cpp @@ -19,7 +19,7 @@ namespace apl { std::shared_ptr -ComponentEventTargetWrapper::create(const std::shared_ptr& component) { +ComponentEventTargetWrapper::create(const ConstCoreComponentPtr& component) { return std::make_shared(component); } diff --git a/aplcore/src/component/componenteventwrapper.cpp b/aplcore/src/component/componenteventwrapper.cpp index 356a9b1..391c88c 100644 --- a/aplcore/src/component/componenteventwrapper.cpp +++ b/aplcore/src/component/componenteventwrapper.cpp @@ -18,7 +18,7 @@ namespace apl { -ComponentEventWrapper::ComponentEventWrapper(const std::shared_ptr& component) +ComponentEventWrapper::ComponentEventWrapper(const ConstCoreComponentPtr& component) : mComponent(component) { } diff --git a/aplcore/src/component/componentproperties.cpp b/aplcore/src/component/componentproperties.cpp index 8b3aae3..4d85130 100644 --- a/aplcore/src/component/componentproperties.cpp +++ b/aplcore/src/component/componentproperties.cpp @@ -287,190 +287,196 @@ Bimap sKeyboardBehaviorOnFocusMap = { * are not part of the public documentation and may change at any time. */ Bimap sComponentPropertyBimap = { - {kPropertyAccessibilityActions, "action"}, - {kPropertyAccessibilityActions, "actions"}, - {kPropertyAccessibilityLabel, "accessibilityLabel"}, - {kPropertyAlign, "align"}, - {kPropertyAlignItems, "alignItems"}, - {kPropertyAlignSelf, "alignSelf"}, - {kPropertyAudioTrack, "audioTrack"}, - {kPropertyAutoplay, "autoplay"}, - {kPropertyMuted, "muted"}, - {kPropertyBackgroundColor, "backgroundColor"}, - {kPropertyBorderBottomLeftRadius, "borderBottomLeftRadius"}, - {kPropertyBorderBottomRightRadius, "borderBottomRightRadius"}, - {kPropertyBorderColor, "borderColor"}, - {kPropertyBorderRadius, "borderRadius"}, - {kPropertyBorderRadii, "_borderRadii"}, - {kPropertyBorderStrokeWidth, "borderStrokeWidth"}, - {kPropertyBorderTopLeftRadius, "borderTopLeftRadius"}, - {kPropertyBorderTopRightRadius, "borderTopRightRadius"}, - {kPropertyBorderWidth, "borderWidth"}, - {kPropertyBottom, "bottom"}, - {kPropertyBounds, "_bounds"}, - {kPropertyCenterId, "centerId"}, - {kPropertyCenterIndex, "centerIndex"}, - {kPropertyChecked, "checked"}, - {kPropertyChildHeight, "childHeight"}, - {kPropertyChildHeight, "childHeights"}, - {kPropertyChildWidth, "childWidth"}, - {kPropertyChildWidth, "childWidths"}, - {kPropertyColor, "color"}, - {kPropertyColorKaraokeTarget, "_colorKaraokeTarget"}, - {kPropertyColorNonKaraoke, "_colorNonKaraoke"}, - {kPropertyCurrentPage, "_currentPage"}, - {kPropertyDescription, "description"}, - {kPropertyDirection, "direction"}, - {kPropertyDisabled, "disabled"}, - {kPropertyDisplay, "display"}, - {kPropertyDrawnBorderWidth, "_drawnBorderWidth"}, - {kPropertyEmbeddedDocument, "embeddedDocument"}, - {kPropertyEnd, "end"}, - {kPropertyEntities, "entities"}, - {kPropertyEntities, "entity"}, - {kPropertyEnvironment, "environment"}, - {kPropertyFastScrollScale, "-fastScrollScale"}, - {kPropertyFilters, "filters"}, - {kPropertyFilters, "filter"}, - {kPropertyFirstId, "firstId"}, - {kPropertyFirstIndex, "firstIndex"}, - {kPropertyFontFamily, "fontFamily"}, - {kPropertyFocusable, "_focusable"}, - {kPropertyFontSize, "fontSize"}, - {kPropertyFontStyle, "fontStyle"}, - {kPropertyGestures, "gestures"}, - {kPropertyGestures, "gesture"}, - {kPropertyHandleTick, "handleTick"}, - {kPropertyHighlightColor, "highlightColor"}, - {kPropertyHint, "hint"}, - {kPropertyHintColor, "hintColor"}, - {kPropertyHintStyle, "hintStyle"}, - {kPropertyHintWeight, "hintWeight"}, - {kPropertyFontWeight, "fontWeight"}, - {kPropertyGraphic, "graphic"}, - {kPropertyGrow, "grow"}, - {kPropertyHandleKeyDown, "handleKeyDown"}, - {kPropertyHandleKeyUp, "handleKeyUp"}, - {kPropertyHandlePageMove, "handlePageMove"}, - {kPropertyHeight, "height"}, - {kPropertyId, "id"}, - {kPropertyInitialPage, "initialPage"}, - {kPropertyInnerBounds, "_innerBounds"}, - {kPropertyItemsPerCourse, "_itemsPerCourse"}, - {kPropertyJustifyContent, "justifyContent"}, - {kPropertyKeyboardBehaviorOnFocus, "-keyboardBehaviorOnFocus"}, - {kPropertyKeyboardType, "keyboardType"}, - {kPropertyLaidOut, "_laidOut"}, - {kPropertyLang, "lang"}, - {kPropertyLayoutDirection, "_layoutDirection"}, - {kPropertyLayoutDirectionAssigned, "layoutDirection"}, - {kPropertyLeft, "left"}, - {kPropertyLetterSpacing, "letterSpacing"}, - {kPropertyLineHeight, "lineHeight"}, - {kPropertyMaxHeight, "maxHeight"}, - {kPropertyMaxLength, "maxLength"}, - {kPropertyMaxLines, "maxLines"}, - {kPropertyMaxWidth, "maxWidth"}, - {kPropertyMediaBounds, "mediaBounds"}, - {kPropertyMediaState, "_mediaState"}, - {kPropertyMinHeight, "minHeight"}, - {kPropertyMinWidth, "minWidth"}, - {kPropertyNavigation, "navigation"}, - {kPropertyNextFocusDown, "nextFocusDown"}, - {kPropertyNextFocusForward, "nextFocusForward"}, - {kPropertyNextFocusLeft, "nextFocusLeft"}, - {kPropertyNextFocusRight, "nextFocusRight"}, - {kPropertyNextFocusUp, "nextFocusUp"}, - {kPropertyNotifyChildrenChanged, "_notify_childrenChanged"}, - {kPropertyNumbered, "numbered"}, - {kPropertyNumbering, "numbering"}, - {kPropertyOnBlur, "onBlur"}, - {kPropertyOnCancel, "onCancel"}, - {kPropertyOnCursorEnter, "onCursorEnter"}, - {kPropertyOnCursorExit, "onCursorExit"}, - {kPropertyOnDown, "onDown"}, - {kPropertyOnEnd, "onEnd"}, - {kPropertyOnFail, "onFail"}, - {kPropertyResourceOnFatalError, "onFatalError"}, - {kPropertyOnFocus, "onFocus"}, - {kPropertyOnLoad, "onLoad"}, - {kPropertyOnMount, "onMount"}, - {kPropertyOnMove, "onMove"}, - {kPropertyOnSpeechMark, "onSpeechMark"}, - {kPropertyOnScroll, "onScroll"}, - {kPropertyOnPageChanged, "onPageChanged"}, - {kPropertyOnPause, "onPause"}, - {kPropertyOnPlay, "onPlay"}, - {kPropertyOnTrackFail, "onTrackFail"}, - {kPropertyOnTrackReady, "onTrackReady"}, - {kPropertyOnPress, "onPress"}, - {kPropertyOnSubmit, "onSubmit"}, - {kPropertyOnTextChange, "onTextChange"}, - {kPropertyOnTimeUpdate, "onTimeUpdate"}, - {kPropertyOnTrackUpdate, "onTrackUpdate"}, - {kPropertyOnUp, "onUp"}, - {kPropertyOpacity, "opacity"}, - {kPropertyOverlayColor, "overlayColor"}, - {kPropertyOverlayGradient, "overlayGradient"}, - {kPropertyPadding, "padding"}, - {kPropertyPaddingBottom, "paddingBottom"}, - {kPropertyPaddingEnd, "paddingEnd"}, - {kPropertyPaddingLeft, "paddingLeft"}, - {kPropertyPaddingRight, "paddingRight"}, - {kPropertyPaddingStart, "paddingStart"}, - {kPropertyPaddingTop, "paddingTop"}, - {kPropertyPageDirection, "pageDirection"}, - {kPropertyPageId, "pageId"}, - {kPropertyPageIndex, "pageIndex"}, - {kPropertyPlayingState, "playingState"}, - {kPropertyPosition, "position"}, - {kPropertyPreserve, "preserve"}, - {kPropertyRangeKaraokeTarget, "_rangeKaraokeTarget"}, - {kPropertyResourceId, "resourceId"}, - {kPropertyResourceState, "_resourceState"}, - {kPropertyResourceType, "_resourceType"}, - {kPropertyRight, "right"}, - {kPropertyRole, "role"}, - {kPropertyScale, "scale"}, - {kPropertyScrollAnimation, "-scrollAnimation"}, - {kPropertyScrollDirection, "scrollDirection"}, - {kPropertyScrollOffset, "scrollOffset"}, - {kPropertyScrollPercent, "scrollPercent"}, - {kPropertyScrollPosition, "_scrollPosition"}, - {kPropertySecureInput, "secureInput"}, - {kPropertySelectOnFocus, "selectOnFocus"}, - {kPropertyShadowColor, "shadowColor"}, - {kPropertyShadowHorizontalOffset, "shadowHorizontalOffset"}, - {kPropertyShadowRadius, "shadowRadius"}, - {kPropertyShadowVerticalOffset, "shadowVerticalOffset"}, - {kPropertyShrink, "shrink"}, - {kPropertySize, "size"}, - {kPropertySnap, "snap"}, - {kPropertySource, "source"}, - {kPropertySource, "sources"}, - {kPropertySpacing, "spacing"}, - {kPropertySpeech, "speech"}, - {kPropertySubmitKeyType, "submitKeyType"}, - {kPropertyStart, "start"}, - {kPropertyText, "text"}, - {kPropertyTextAlign, "_textAlign"}, - {kPropertyTextAlignAssigned, "textAlign"}, - {kPropertyTextAlignVertical, "textAlignVertical"}, - {kPropertyTop, "top"}, - {kPropertyTrackCount, "_trackCount"}, - {kPropertyTrackCurrentTime, "_trackCurrentTime"}, - {kPropertyTrackDuration, "_trackDuration"}, - {kPropertyTrackEnded, "_trackEnded"}, - {kPropertyTrackIndex, "_trackIndex"}, - {kPropertyTrackPaused, "_trackPaused"}, - {kPropertyTrackState, "_trackState"}, - {kPropertyTransformAssigned, "transform"}, - {kPropertyTransform, "_transform"}, - {kPropertyUser, "_user"}, - {kPropertyValidCharacters, "validCharacters"}, - {kPropertyVisualHash, "_visualHash"}, - {kPropertyWidth, "width"}, - {kPropertyWrap, "wrap"}, + {kPropertyAccessibilityActions, "_actions"}, + {kPropertyAccessibilityActionsAssigned, "action"}, + {kPropertyAccessibilityActionsAssigned, "actions"}, + {kPropertyAccessibilityLabel, "accessibilityLabel"}, + {kPropertyAlign, "align"}, + {kPropertyAlignItems, "alignItems"}, + {kPropertyAlignSelf, "alignSelf"}, + {kPropertyAudioTrack, "audioTrack"}, + {kPropertyAutoplay, "autoplay"}, + {kPropertyMuted, "muted"}, + {kPropertyBackgroundColor, "backgroundColor"}, + {kPropertyBackgroundAssigned, "background"}, + {kPropertyBackground, "_background"}, + {kPropertyBorderBottomLeftRadius, "borderBottomLeftRadius"}, + {kPropertyBorderBottomRightRadius, "borderBottomRightRadius"}, + {kPropertyBorderColor, "borderColor"}, + {kPropertyBorderRadius, "borderRadius"}, + {kPropertyBorderRadii, "_borderRadii"}, + {kPropertyBorderStrokeWidth, "borderStrokeWidth"}, + {kPropertyBorderTopLeftRadius, "borderTopLeftRadius"}, + {kPropertyBorderTopRightRadius, "borderTopRightRadius"}, + {kPropertyBorderWidth, "borderWidth"}, + {kPropertyBottom, "bottom"}, + {kPropertyBounds, "_bounds"}, + {kPropertyCenterId, "centerId"}, + {kPropertyCenterIndex, "centerIndex"}, + {kPropertyChecked, "checked"}, + {kPropertyChildHeight, "childHeight"}, + {kPropertyChildHeight, "childHeights"}, + {kPropertyChildWidth, "childWidth"}, + {kPropertyChildWidth, "childWidths"}, + {kPropertyColor, "color"}, + {kPropertyColorKaraokeTarget, "_colorKaraokeTarget"}, + {kPropertyColorNonKaraoke, "_colorNonKaraoke"}, + {kPropertyCurrentPage, "_currentPage"}, + {kPropertyDescription, "description"}, + {kPropertyDirection, "direction"}, + {kPropertyDisabled, "disabled"}, + {kPropertyDisplay, "display"}, + {kPropertyDrawnBorderWidth, "_drawnBorderWidth"}, + {kPropertyEmbeddedDocument, "embeddedDocument"}, + {kPropertyEnd, "end"}, + {kPropertyEntities, "entities"}, + {kPropertyEntities, "entity"}, + {kPropertyEnvironment, "environment"}, + {kPropertyFastScrollScale, "-fastScrollScale"}, + {kPropertyFilters, "filters"}, + {kPropertyFilters, "filter"}, + {kPropertyFirstId, "firstId"}, + {kPropertyFirstIndex, "firstIndex"}, + {kPropertyFontFamily, "fontFamily"}, + {kPropertyFocusable, "_focusable"}, + {kPropertyFontSize, "fontSize"}, + {kPropertyFontStyle, "fontStyle"}, + {kPropertyGestures, "gestures"}, + {kPropertyGestures, "gesture"}, + {kPropertyHandleTick, "handleTick"}, + {kPropertyHighlightColor, "highlightColor"}, + {kPropertyHint, "hint"}, + {kPropertyHintColor, "hintColor"}, + {kPropertyHintStyle, "hintStyle"}, + {kPropertyHintWeight, "hintWeight"}, + {kPropertyFontWeight, "fontWeight"}, + {kPropertyGraphic, "graphic"}, + {kPropertyGrow, "grow"}, + {kPropertyHandleKeyDown, "handleKeyDown"}, + {kPropertyHandleKeyUp, "handleKeyUp"}, + {kPropertyHandlePageMove, "handlePageMove"}, + {kPropertyHeight, "height"}, + {kPropertyId, "id"}, + {kPropertyInitialPage, "initialPage"}, + {kPropertyInnerBounds, "_innerBounds"}, + {kPropertyItemsPerCourse, "_itemsPerCourse"}, + {kPropertyJustifyContent, "justifyContent"}, + {kPropertyKeyboardBehaviorOnFocus, "-keyboardBehaviorOnFocus"}, + {kPropertyKeyboardType, "keyboardType"}, + {kPropertyLaidOut, "_laidOut"}, + {kPropertyLang, "lang"}, + {kPropertyLayoutDirection, "_layoutDirection"}, + {kPropertyLayoutDirectionAssigned, "layoutDirection"}, + {kPropertyLeft, "left"}, + {kPropertyLetterSpacing, "letterSpacing"}, + {kPropertyLineHeight, "lineHeight"}, + {kPropertyMaxHeight, "maxHeight"}, + {kPropertyMaxLength, "maxLength"}, + {kPropertyMaxLines, "maxLines"}, + {kPropertyMaxWidth, "maxWidth"}, + {kPropertyMediaBounds, "mediaBounds"}, + {kPropertyMediaState, "_mediaState"}, + {kPropertyMinHeight, "minHeight"}, + {kPropertyMinWidth, "minWidth"}, + {kPropertyNavigation, "navigation"}, + {kPropertyNextFocusDown, "nextFocusDown"}, + {kPropertyNextFocusForward, "nextFocusForward"}, + {kPropertyNextFocusLeft, "nextFocusLeft"}, + {kPropertyNextFocusRight, "nextFocusRight"}, + {kPropertyNextFocusUp, "nextFocusUp"}, + {kPropertyNotifyChildrenChanged, "_notify_childrenChanged"}, + {kPropertyNumbered, "numbered"}, + {kPropertyNumbering, "numbering"}, + {kPropertyOnBlur, "onBlur"}, + {kPropertyOnCancel, "onCancel"}, + {kPropertyOnChildrenChanged, "onChildrenChanged"}, + {kPropertyOnCursorEnter, "onCursorEnter"}, + {kPropertyOnCursorExit, "onCursorExit"}, + {kPropertyOnDown, "onDown"}, + {kPropertyOnEnd, "onEnd"}, + {kPropertyOnFail, "onFail"}, + {kPropertyResourceOnFatalError, "onFatalError"}, + {kPropertyOnFocus, "onFocus"}, + {kPropertyOnLoad, "onLoad"}, + {kPropertyOnMount, "onMount"}, + {kPropertyOnMove, "onMove"}, + {kPropertyOnSpeechMark, "onSpeechMark"}, + {kPropertyOnScroll, "onScroll"}, + {kPropertyOnPageChanged, "onPageChanged"}, + {kPropertyOnPause, "onPause"}, + {kPropertyOnPlay, "onPlay"}, + {kPropertyOnTrackFail, "onTrackFail"}, + {kPropertyOnTrackReady, "onTrackReady"}, + {kPropertyOnPress, "onPress"}, + {kPropertyOnSubmit, "onSubmit"}, + {kPropertyOnTextChange, "onTextChange"}, + {kPropertyOnTimeUpdate, "onTimeUpdate"}, + {kPropertyOnTrackUpdate, "onTrackUpdate"}, + {kPropertyOnUp, "onUp"}, + {kPropertyOpacity, "opacity"}, + {kPropertyOverlayColor, "overlayColor"}, + {kPropertyOverlayGradient, "overlayGradient"}, + {kPropertyPadding, "padding"}, + {kPropertyPaddingBottom, "paddingBottom"}, + {kPropertyPaddingEnd, "paddingEnd"}, + {kPropertyPaddingLeft, "paddingLeft"}, + {kPropertyPaddingRight, "paddingRight"}, + {kPropertyPaddingStart, "paddingStart"}, + {kPropertyPaddingTop, "paddingTop"}, + {kPropertyPageDirection, "pageDirection"}, + {kPropertyPageId, "pageId"}, + {kPropertyPageIndex, "pageIndex"}, + {kPropertyParameters, "parameters"}, + {kPropertyParameters, "parameter"}, + {kPropertyPlayingState, "playingState"}, + {kPropertyPosition, "position"}, + {kPropertyPreserve, "preserve"}, + {kPropertyRangeKaraokeTarget, "_rangeKaraokeTarget"}, + {kPropertyResourceId, "resourceId"}, + {kPropertyResourceState, "_resourceState"}, + {kPropertyResourceType, "_resourceType"}, + {kPropertyRight, "right"}, + {kPropertyRole, "role"}, + {kPropertyScale, "scale"}, + {kPropertyScrollAnimation, "-scrollAnimation"}, + {kPropertyScrollDirection, "scrollDirection"}, + {kPropertyScrollOffset, "scrollOffset"}, + {kPropertyScrollPercent, "scrollPercent"}, + {kPropertyScrollPosition, "_scrollPosition"}, + {kPropertySecureInput, "secureInput"}, + {kPropertySelectOnFocus, "selectOnFocus"}, + {kPropertyShadowColor, "shadowColor"}, + {kPropertyShadowHorizontalOffset, "shadowHorizontalOffset"}, + {kPropertyShadowRadius, "shadowRadius"}, + {kPropertyShadowVerticalOffset, "shadowVerticalOffset"}, + {kPropertyShrink, "shrink"}, + {kPropertySize, "size"}, + {kPropertySnap, "snap"}, + {kPropertySource, "source"}, + {kPropertySource, "sources"}, + {kPropertySpacing, "spacing"}, + {kPropertySpeech, "speech"}, + {kPropertySubmitKeyType, "submitKeyType"}, + {kPropertyStart, "start"}, + {kPropertyText, "text"}, + {kPropertyTextAlign, "_textAlign"}, + {kPropertyTextAlignAssigned, "textAlign"}, + {kPropertyTextAlignVertical, "textAlignVertical"}, + {kPropertyTop, "top"}, + {kPropertyTrackCount, "_trackCount"}, + {kPropertyTrackCurrentTime, "_trackCurrentTime"}, + {kPropertyTrackDuration, "_trackDuration"}, + {kPropertyTrackEnded, "_trackEnded"}, + {kPropertyTrackIndex, "_trackIndex"}, + {kPropertyTrackPaused, "_trackPaused"}, + {kPropertyTrackState, "_trackState"}, + {kPropertyTransformAssigned, "transform"}, + {kPropertyTransform, "_transform"}, + {kPropertyUser, "_user"}, + {kPropertyValidCharacters, "validCharacters"}, + {kPropertyVisualHash, "_visualHash"}, + {kPropertyWidth, "width"}, + {kPropertyWrap, "wrap"}, }; Bimap sComponentTypeBimap = { diff --git a/aplcore/src/component/corecomponent.cpp b/aplcore/src/component/corecomponent.cpp index 80bfb07..e27a03c 100644 --- a/aplcore/src/component/corecomponent.cpp +++ b/aplcore/src/component/corecomponent.cpp @@ -62,6 +62,13 @@ const std::string VISUAL_CONTEXT_TYPE_EMPTY = "empty"; /*****************************************************************/ +const std::string CHILDREN_CHANGE_UID = "uid"; +const std::string CHILDREN_CHANGE_INDEX = "index"; +const std::string CHILDREN_CHANGE_ACTION = "action"; +const std::string CHILDREN_CHANGE_CHANGES = "changes"; + +/*****************************************************************/ + const static bool DEBUG_BOUNDS = false; const static bool DEBUG_ENSURE = false; const static bool DEBUG_LAYOUTDIRECTION = false; @@ -112,14 +119,15 @@ CoreComponent::CoreComponent(const ContextPtr& context, mYGNodeRef(YGNodeNewWithConfig(context->ygconfig())), mPath(path), mDisplayedChildrenStale(true), + mIsDisallowed(false), mGlobalToLocalIsStale(true), mTextMeasurementHashStale(true), mVisualHashStale(true) { YGNodeSetContext(mYGNodeRef, this); } -std::shared_ptr -CoreComponent::cast( const std::shared_ptr& component) { +CoreComponentPtr +CoreComponent::cast( const ComponentPtr& component) { return std::static_pointer_cast(component); } @@ -152,7 +160,7 @@ CoreComponent::processTickHandlers() { for (const auto& handler : tickHandlers.getArray()) { auto delay = std::max(propertyAsDouble(*mContext, handler, "minimumDelay", 1000), - mContext->getRootConfig().getTickHandlerUpdateLimit()); + mContext->getRootConfig().getProperty(RootProperty::kTickHandlerUpdateLimit).getDouble()); scheduleTickHandler(handler, delay); } } @@ -194,15 +202,19 @@ CoreComponent::initialize() // Set notion of focusability. Here as we actually may need to know all other properties. mCalculated.set(kPropertyFocusable, isFocusable()); - // The component property definition set returns an array of raw accessibility action objects. This code - // processes the raw array, sets up dependancy relationships, and stores the processed array under the same key. + // The component property definition set returns an array of raw accessibility action objects. + // This code processes the raw array, sets up dependancy relationships, if any, and stores the + // processed array under the same key. ObjectArray actions; - for (const auto& m : mCalculated.get(kPropertyAccessibilityActions).getArray() ) { + for (const auto& m : mCalculated.get(kPropertyAccessibilityActionsAssigned).getArray() ) { auto aa = AccessibilityAction::create(shared_from_corecomponent(), m); if (aa) actions.emplace_back(std::move(aa)); } - mCalculated.set(kPropertyAccessibilityActions, Object(std::move(actions))); + mCalculated.set(kPropertyAccessibilityActionsAssigned, Object(std::move(actions))); + + if (!getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureDynamicAccessibilityActions)) + fixAccessibilityActions(); // Process tick handlers here. Not same as onMount as it's a bad idea to go through every component on every tick // to collect handlers and run them on mass. @@ -400,16 +412,11 @@ CoreComponent::appendChild(const ComponentPtr& child, bool useDirtyFlag) } void -CoreComponent::notifyChildChanged(size_t index, const std::string& uid, const std::string& action) +CoreComponent::notifyChildChanged(size_t index, const CoreComponentPtr& component, ChildChangeAction action) { - auto &changes = mCalculated.get(kPropertyNotifyChildrenChanged).getMutableArray(); - auto change = std::make_shared(); - change->emplace("index", index); - change->emplace("uid", uid); - change->emplace("action", action); - changes.emplace_back(change); - - setDirty(kPropertyNotifyChildrenChanged); + mChildrenChanges.emplace_back(ChildChange{component, action, index}); + // Mark component as dirty so required processing will take place + mContext->setDirty(shared_from_this()); } void @@ -544,13 +551,13 @@ CoreComponent::insertChild(const CoreComponentPtr& child, size_t index, bool use mChildren.insert(mChildren.begin() + index, coreChild); + coreChild->attachedToParent(shared_from_corecomponent()); if (useDirtyFlag) { - notifyChildChanged(index, child->getUniqueId(), "insert"); + notifyChildChanged(index, child, kChildChangeActionInsert); // If we add a view hierarchy with dirty flags, we need to update the context coreChild->markAdded(); } - coreChild->attachedToParent(shared_from_corecomponent()); coreChild->markGlobalToLocalTransformStale(); markDisplayedChildrenStale(useDirtyFlag); setVisualContextDirty(); @@ -605,7 +612,7 @@ CoreComponent::removeChildAfterMarkedRemoved(const CoreComponentPtr& child, size // The parent component has changed the number of children if (useDirtyFlag) - notifyChildChanged(index, child->getUniqueId(), "remove"); + notifyChildChanged(index, child, kChildChangeActionRemove); markDisplayedChildrenStale(useDirtyFlag); mDisplayedChildren.clear(); @@ -867,6 +874,19 @@ CoreComponent::constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) } #endif // SCENEGRAPH +bool +CoreComponent::isParentOf(const CoreComponentPtr& child) +{ + auto self = shared_from_this(); + + ComponentPtr parent = child->getParent(); + while (parent && parent != self) { + parent = parent->getParent(); + } + + return self == parent; +} + /** * Initial assignment of properties. Don't set any dirty flags here; this * all should be running in the constructor. @@ -991,6 +1011,9 @@ CoreComponent::handlePropertyChange(const ComponentPropDef& def, const Object& v if ((def.flags & kPropVisualHash) != 0) mVisualHashStale = true; + if ((def.flags & kPropAccessibility) != 0) + markAccessibilityDirty(); + // Properties with the kPropOut flag mark the property as dirty if ((def.flags & kPropOut) != 0) setDirty(def.key); @@ -1191,7 +1214,7 @@ CoreComponent::getPropertyAndWriteableState(const std::string& key) const } Object -CoreComponent::getProperty(PropertyKey key) +CoreComponent::getProperty(PropertyKey key) const { if (sComponentPropertyBimap.has(key)) { auto findRef = find(key); @@ -1402,6 +1425,10 @@ CoreComponent::setDirty( PropertyKey key ) if (!mVisualHashStale && (def->second.flags & kPropVisualHash)) { mVisualHashStale = true; } + + if (def->second.flags & kPropAccessibility) { + markAccessibilityDirty(); + } } } } @@ -1496,9 +1523,9 @@ CoreComponent::needsLayout() const } bool -CoreComponent::shouldNotPropagateLayoutChanges() const +CoreComponent::shouldPropagateLayoutChanges() const { - return mChildren.empty() || static_cast(getCalculated(kPropertyDisplay).getInteger()) == kDisplayNone; + return !mChildren.empty() && static_cast(getCalculated(kPropertyDisplay).getInteger()) != kDisplayNone; } std::string @@ -1627,22 +1654,86 @@ CoreComponent::processLayoutChanges(bool useDirtyFlag, bool first) setDirty(kPropertyInnerBounds); } + if (shouldPropagateLayoutChanges()) { + // Inform all children that they should re-check their bounds. No need to do that for not + // attached ones. Note that children of a Pager are not attached, and hence they will not + // be processed. + for (auto& child : mChildren) + if (child->isAttached()) + child->processLayoutChanges(useDirtyFlag, first); + } + if (!mCalculated.get(kPropertyLaidOut).asBoolean() && !mCalculated.get(kPropertyBounds).get().empty()) { mCalculated.set(kPropertyLaidOut, true); if (useDirtyFlag) setDirty(kPropertyLaidOut); + markAccessibilityDirty(); } +} - // Break out early if possible - there are no need to propagate to children - if (shouldNotPropagateLayoutChanges()) return; +/** + * Note: It may be relatively expensive to run this operation, as it's executed for every dirty + * component on clearPending. Any operation performed has to break out early if no change is + * required. + */ +void +CoreComponent::postClearPending() +{ + // Process and report DOM (not layout) changes. + processChildrenChanges(); + refreshAccessibilityActions(true); +} - // Inform all children that they should re-check their bounds. No need to do that for not attached ones. - // Note that children of a Pager are not attached, and hence they will not be processed. - for (auto& child : mChildren) - if (child->isAttached()) - child->processLayoutChanges(useDirtyFlag, first); +std::string +CoreComponent::toStringAction(ChildChangeAction action) { + return action == kChildChangeActionInsert ? "insert" : "remove"; } +void +CoreComponent::processChildrenChanges() { + if (mChildrenChanges.empty()) + return; + + // Report children changes to the runtime + { + auto& changes = mCalculated.get(kPropertyNotifyChildrenChanged).getMutableArray(); + for (const auto& c : mChildrenChanges) { + auto change = std::make_shared(); + change->emplace(CHILDREN_CHANGE_INDEX, c.index); + change->emplace(CHILDREN_CHANGE_UID, c.component->getUniqueId()); + change->emplace(CHILDREN_CHANGE_ACTION, toStringAction(c.action)); + changes.emplace_back(change); + } + + setDirty(kPropertyNotifyChildrenChanged); + } + + // Execute ChildrenChanged handler, if defined + auto commands = mCalculated.get(kPropertyOnChildrenChanged); + if (multiChild() && !commands.empty()) { + auto handlerChanges = std::make_shared(); + for (const auto& c : mChildrenChanges) { + auto change = std::make_shared(); + if (c.action == kChildChangeActionInsert) + change->emplace( + CHILDREN_CHANGE_INDEX, + getChildIndex(c.component) + ); + change->emplace(CHILDREN_CHANGE_UID, c.component->getUniqueId()); + change->emplace(CHILDREN_CHANGE_ACTION, toStringAction(c.action)); + handlerChanges->emplace_back(change); + } + + auto changesMap = std::make_shared(); + changesMap->emplace(CHILDREN_CHANGE_CHANGES, handlerChanges); + mContext->sequencer().executeCommands( + commands, + createEventContext("ChildrenChanged", changesMap, getValue()), + shared_from_corecomponent(), true); + + } + mChildrenChanges.clear(); +} void CoreComponent::postProcessLayoutChanges() @@ -2196,7 +2287,8 @@ CoreComponent::calculateRealOpacity() const bool CoreComponent::isDisplayable() const { return (getCalculated(kPropertyDisplay).asInt() == kDisplayNormal) - && (getCalculated(kPropertyOpacity).asNumber() > 0); + && (getCalculated(kPropertyOpacity).asNumber() > 0) + && !mIsDisallowed; } void @@ -2410,123 +2502,217 @@ CoreComponent::textBaselineFunc( YGNodeRef node, float width, float height ) return component->textBaselineInternal(width, height); } +// Old style static actions. Just copy defined and implicit to the output. +void +CoreComponent::fixAccessibilityActions() { + auto current = getCalculated(kPropertyAccessibilityActionsAssigned).getArray(); + + ObjectArray result; + // Copy all defined + for (const auto& ao : current) { + result.emplace_back(ao); + } + + std::map supportedActions; + getSupportedStandardAccessibilityActions(supportedActions); + + // Copy all implicit + for (const auto& a : supportedActions) { + if (a.second) + result.emplace_back(AccessibilityAction::create(shared_from_corecomponent(), + a.first, a.first)); + } + + setCalculated(kPropertyAccessibilityActions, std::move(result)); +} + +void +CoreComponent::markAccessibilityDirty() +{ + if (!getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureDynamicAccessibilityActions)) + return; + + mContext->setDirty(shared_from_this()); + mAccessibilityDirty = true; +} + +// New style dynamic actions +void +CoreComponent::refreshAccessibilityActions(bool useDirtyFlag) +{ + if (!getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureDynamicAccessibilityActions)) + return; + + if (!mAccessibilityDirty) return; + mAccessibilityDirty = false; + + auto current = getCalculated(kPropertyAccessibilityActions).getArray(); + if (getCalculated(kPropertyDisabled).asBoolean()) { + if (!current.empty()) { + mCalculated.set(kPropertyAccessibilityActions, Object::EMPTY_ARRAY()); + if (useDirtyFlag) setDirty(kPropertyAccessibilityActions); + } + return; + } + + auto cmp = [](const AccessibilityActionPtr& left, const AccessibilityActionPtr& right) { + return left->getName() < right->getName(); + }; + + // Dedupe the list by the Action name, only first definition considered valid + std::set deduped(cmp); + + // Add defined actions + for (const auto& m : mCalculated.get(kPropertyAccessibilityActionsAssigned).getArray() ) { + deduped.emplace(m.get()); + } + + // Get component supported standard actions (may be based on component state) + std::map supportedActions; + getSupportedStandardAccessibilityActions(supportedActions); + + // Add supported actions if implicit (does not have to be defined) + for (const auto& m : supportedActions) { + if (m.second) + deduped.emplace(AccessibilityAction::create(shared_from_corecomponent(), m.first, m.first)); + } + + // There are 3 types of valid actions: + // 1. Enabled custom + // 2. Enabled standard action (component or gesture) + // 3. Implicit actions (like scrolling), unless disabled + ObjectArray result; + for (const auto& aa : deduped) { + if (aa->enabled() && (!aa->getCommands().empty() || supportedActions.count(aa->getName()))) + result.emplace_back(aa); + } + + if (current != result) { + mCalculated.set(kPropertyAccessibilityActions, Object(std::move(result))); + if (useDirtyFlag) setDirty(kPropertyAccessibilityActions); + } +} + const ComponentPropDefSet& CoreComponent::propDefSet() const { static ComponentPropDefSet sCommonComponentProperties = ComponentPropDefSet().add({ - {kPropertyAccessibilityLabel, "", asString, kPropInOut | - kPropDynamic}, - {kPropertyAccessibilityActions, Object::EMPTY_ARRAY(), asArray, kPropInOut}, - {kPropertyBounds, Rect(0,0,0,0), nullptr, kPropOut | - kPropVisualContext | - kPropVisualHash}, - {kPropertyChecked, false, asBoolean, kPropInOut | - kPropDynamic | - kPropMixedState | - kPropVisualContext}, - {kPropertyDescription, "", asString, kPropIn}, - {kPropertyDisplay, kDisplayNormal, sDisplayMap, kPropInOut | - kPropStyled | - kPropDynamic | - kPropVisualContext, yn::setDisplay}, - {kPropertyDisabled, false, asBoolean, kPropInOut | - kPropDynamic | - kPropMixedState | - kPropVisualContext}, - {kPropertyEntities, Object::EMPTY_ARRAY(), asDeepArray, kPropIn | - kPropDynamic | - kPropEvaluated | - kPropVisualContext}, - {kPropertyFocusable, false, nullptr, kPropOut}, - {kPropertyHandleTick, Object::EMPTY_ARRAY(), asArray, kPropIn}, - {kPropertyHeight, Dimension(), asDimension, kPropIn | - kPropDynamic | - kPropStyled, yn::setHeight, defaultHeight}, - {kPropertyInnerBounds, Rect(0,0,0,0), nullptr, kPropOut | - kPropVisualContext | - kPropVisualHash}, - {kPropertyLayoutDirectionAssigned, kLayoutDirectionInherit, sLayoutDirectionMap, kPropIn | - kPropDynamic | - kPropStyled, yn::setLayoutDirection}, - {kPropertyLayoutDirection, kLayoutDirectionLTR, sLayoutDirectionMap, kPropOut | - kPropTextHash | - kPropVisualHash}, - {kPropertyMaxHeight, Object::NULL_OBJECT(), asNonAutoDimension, kPropIn | - kPropDynamic | - kPropStyled, yn::setMaxHeight}, - {kPropertyMaxWidth, Object::NULL_OBJECT(), asNonAutoDimension, kPropIn | - kPropDynamic | - kPropStyled, yn::setMaxWidth}, - {kPropertyMinHeight, Dimension(0), asNonAutoDimension, kPropIn | - kPropDynamic | - kPropStyled, yn::setMinHeight}, - {kPropertyMinWidth, Dimension(0), asNonAutoDimension, kPropIn | - kPropDynamic | - kPropStyled, yn::setMinWidth}, - {kPropertyOnMount, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyOnSpeechMark, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyOpacity, 1.0, asOpacity, kPropInOut | - kPropStyled | - kPropDynamic | - kPropVisualContext | - kPropVisualHash}, - {kPropertyPadding, Object::EMPTY_ARRAY(), asPaddingArray, kPropIn | - kPropDynamic | - kPropStyled, inlineFixPadding}, - {kPropertyPaddingBottom, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | - kPropDynamic | - kPropStyled, inlineFixPadding}, - {kPropertyPaddingLeft, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | - kPropDynamic | - kPropStyled, inlineFixPadding}, - {kPropertyPaddingRight, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | - kPropDynamic | - kPropStyled, inlineFixPadding}, - {kPropertyPaddingTop, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | - kPropDynamic | - kPropStyled, inlineFixPadding}, - {kPropertyPaddingStart, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | - kPropDynamic | - kPropStyled, inlineFixPadding}, - {kPropertyPaddingEnd, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | - kPropDynamic | - kPropStyled, inlineFixPadding}, - {kPropertyPreserve, Object::EMPTY_ARRAY(), asArray, kPropIn}, - {kPropertyRole, kRoleNone, sRoleMap, kPropInOut | - kPropStyled}, - {kPropertyShadowColor, Color(), asColor, kPropInOut | - kPropDynamic | - kPropStyled | - kPropVisualHash}, - {kPropertyShadowHorizontalOffset, Dimension(0), asAbsoluteDimension, kPropInOut | - kPropDynamic | - kPropStyled | - kPropVisualHash}, - {kPropertyShadowRadius, Dimension(0), asAbsoluteDimension, kPropInOut | - kPropDynamic | - kPropStyled | - kPropVisualHash}, - {kPropertyShadowVerticalOffset, Dimension(0), asAbsoluteDimension, kPropInOut | - kPropDynamic | - kPropStyled | - kPropVisualHash}, - {kPropertySpeech, "", asString, kPropIn | - kPropVisualContext}, - {kPropertyTransformAssigned, Object::NULL_OBJECT(), asTransformOrArray, kPropIn | - kPropDynamic | - kPropEvaluated | - kPropVisualContext, inlineFixTransform}, - {kPropertyTransform, Transform2D(), nullptr, kPropOut | - kPropVisualContext}, - {kPropertyUser, Object::NULL_OBJECT(), nullptr, kPropOut}, - {kPropertyWidth, Dimension(), asDimension, kPropIn | - kPropDynamic | - kPropStyled, yn::setWidth, defaultWidth}, - {kPropertyOnCursorEnter, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyOnCursorExit, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyLaidOut, false, asBoolean, kPropOut | - kPropVisualContext}, - {kPropertyVisualHash, "", asString, kPropOut | - kPropRuntimeState}, + {kPropertyAccessibilityLabel, "", asString, kPropInOut | + kPropDynamic}, + {kPropertyAccessibilityActions, Object::EMPTY_ARRAY(), asArray, kPropOut | kPropAccessibility}, + {kPropertyAccessibilityActionsAssigned, Object::EMPTY_ARRAY(), asArray, kPropIn}, + {kPropertyBounds, Rect(0,0,0,0), nullptr, kPropOut | + kPropVisualContext | + kPropVisualHash}, + {kPropertyChecked, false, asBoolean, kPropInOut | + kPropDynamic | + kPropMixedState | + kPropVisualContext}, + {kPropertyDescription, "", asString, kPropIn}, + {kPropertyDisplay, kDisplayNormal, sDisplayMap, kPropInOut | + kPropStyled | + kPropDynamic | + kPropVisualContext, yn::setDisplay}, + {kPropertyDisabled, false, asBoolean, kPropInOut | + kPropDynamic | + kPropMixedState | + kPropVisualContext | + kPropAccessibility}, + {kPropertyEntities, Object::EMPTY_ARRAY(), asDeepArray, kPropIn | + kPropDynamic | + kPropEvaluated | + kPropVisualContext}, + {kPropertyFocusable, false, nullptr, kPropOut}, + {kPropertyHandleTick, Object::EMPTY_ARRAY(), asArray, kPropIn}, + {kPropertyHeight, Dimension(), asDimension, kPropIn | + kPropDynamic | + kPropStyled, yn::setHeight, defaultHeight}, + {kPropertyInnerBounds, Rect(0,0,0,0), nullptr, kPropOut | + kPropVisualContext | + kPropVisualHash}, + {kPropertyLayoutDirectionAssigned, kLayoutDirectionInherit, sLayoutDirectionMap, kPropIn | + kPropDynamic | + kPropStyled, yn::setLayoutDirection}, + {kPropertyLayoutDirection, kLayoutDirectionLTR, sLayoutDirectionMap, kPropOut | + kPropTextHash | + kPropVisualHash}, + {kPropertyMaxHeight, Object::NULL_OBJECT(), asNonAutoDimension, kPropIn | + kPropDynamic | + kPropStyled, yn::setMaxHeight}, + {kPropertyMaxWidth, Object::NULL_OBJECT(), asNonAutoDimension, kPropIn | + kPropDynamic | + kPropStyled, yn::setMaxWidth}, + {kPropertyMinHeight, Dimension(0), asNonAutoDimension, kPropIn | + kPropDynamic | + kPropStyled, yn::setMinHeight}, + {kPropertyMinWidth, Dimension(0), asNonAutoDimension, kPropIn | + kPropDynamic | + kPropStyled, yn::setMinWidth}, + {kPropertyOnChildrenChanged, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnMount, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnSpeechMark, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOpacity, 1.0, asOpacity, kPropInOut | + kPropStyled | + kPropDynamic | + kPropVisualContext | + kPropVisualHash}, + {kPropertyPadding, Object::EMPTY_ARRAY(), asPaddingArray, kPropIn | + kPropDynamic | + kPropStyled, inlineFixPadding}, + {kPropertyPaddingBottom, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | + kPropDynamic | + kPropStyled, inlineFixPadding}, + {kPropertyPaddingLeft, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | + kPropDynamic | + kPropStyled, inlineFixPadding}, + {kPropertyPaddingRight, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | + kPropDynamic | + kPropStyled, inlineFixPadding}, + {kPropertyPaddingTop, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | + kPropDynamic | + kPropStyled, inlineFixPadding}, + {kPropertyPaddingStart, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | + kPropDynamic | + kPropStyled, inlineFixPadding}, + {kPropertyPaddingEnd, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | + kPropDynamic | + kPropStyled, inlineFixPadding}, + {kPropertyPreserve, Object::EMPTY_ARRAY(), asArray, kPropIn}, + {kPropertyRole, kRoleNone, sRoleMap, kPropInOut | + kPropStyled}, + {kPropertyShadowColor, Color(), asColor, kPropInOut | + kPropDynamic | + kPropStyled | + kPropVisualHash}, + {kPropertyShadowHorizontalOffset, Dimension(0), asAbsoluteDimension, kPropInOut | + kPropDynamic | + kPropStyled | + kPropVisualHash}, + {kPropertyShadowRadius, Dimension(0), asAbsoluteDimension, kPropInOut | + kPropDynamic | + kPropStyled | + kPropVisualHash}, + {kPropertyShadowVerticalOffset, Dimension(0), asAbsoluteDimension, kPropInOut | + kPropDynamic | + kPropStyled | + kPropVisualHash}, + {kPropertySpeech, "", asAny, kPropIn | + kPropVisualContext}, + {kPropertyTransformAssigned, Object::NULL_OBJECT(), asTransformOrArray, kPropIn | + kPropDynamic | + kPropEvaluated | + kPropVisualContext, inlineFixTransform}, + {kPropertyTransform, Transform2D(), nullptr, kPropOut | + kPropVisualContext}, + {kPropertyUser, Object::NULL_OBJECT(), nullptr, kPropOut}, + {kPropertyWidth, Dimension(), asDimension, kPropIn | + kPropDynamic | + kPropStyled, yn::setWidth, defaultWidth}, + {kPropertyOnCursorEnter, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnCursorExit, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyLaidOut, false, asBoolean, kPropOut | + kPropVisualContext}, + {kPropertyVisualHash, "", asString, kPropOut | + kPropRuntimeState}, }); return sCommonComponentProperties; diff --git a/aplcore/src/component/edittextcomponent.cpp b/aplcore/src/component/edittextcomponent.cpp index 46eb35f..8f382b1 100644 --- a/aplcore/src/component/edittextcomponent.cpp +++ b/aplcore/src/component/edittextcomponent.cpp @@ -53,6 +53,7 @@ EditTextComponent::EditTextComponent(const ContextPtr& context, const Path& path) : ActionableComponent(context, std::move(properties), path) { + mIsDisallowed = context->getRootConfig().getProperty(RootProperty::kDisallowEditText).asBoolean(); #ifdef SCENEGRAPH static auto sgTextMeasureFunc = [](YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) -> YGSize { @@ -123,7 +124,7 @@ static inline Object defaultFontColor(Component& component, const RootConfig& ro static inline Object defaultFontFamily(Component& component, const RootConfig& rootConfig) { - return Object(rootConfig.getDefaultFontFamily()); + return rootConfig.getProperty(RootProperty::kDefaultFontFamily); } static inline Object inheritLang(Component& comp, const RootConfig& rconfig) diff --git a/aplcore/src/component/framecomponent.cpp b/aplcore/src/component/framecomponent.cpp index 1aed076..e6fad41 100644 --- a/aplcore/src/component/framecomponent.cpp +++ b/aplcore/src/component/framecomponent.cpp @@ -54,6 +54,38 @@ FrameComponent::FrameComponent(const ContextPtr& context, YGNodeStyleSetAlignItems(mYGNodeRef, YGAlignFlexStart); } +void +FrameComponent::fixBackgroundByColor(bool useDirtyFlag) +{ + if (getCalculated(kPropertyBackgroundAssigned).isNull()) { + mCalculated.set(kPropertyBackground, getCalculated(kPropertyBackgroundColor)); + if (useDirtyFlag) setDirty(kPropertyBackground); + } +} + +void +FrameComponent::fixBackground(bool useDirtyFlag) +{ + if (!getCalculated(kPropertyBackgroundAssigned).isNull()) { + mCalculated.set(kPropertyBackground, getCalculated(kPropertyBackgroundAssigned)); + if (useDirtyFlag) setDirty(kPropertyBackground); + } +} + +static inline void +inlineFixBackgroundByColor(Component& component) +{ + auto& core = (FrameComponent&)component; + core.fixBackgroundByColor(true); +} + +static inline void +inlineFixBackground(Component& component) +{ + auto& core = (FrameComponent&)component; + core.fixBackground(true); +} + const ComponentPropDefSet& FrameComponent::propDefSet() const { @@ -61,6 +93,11 @@ FrameComponent::propDefSet() const {kPropertyBackgroundColor, Color(), asColor, kPropInOut | kPropStyled | kPropDynamic | + kPropVisualHash, inlineFixBackgroundByColor}, + {kPropertyBackgroundAssigned, Object::NULL_OBJECT(), asFill, kPropIn | + kPropStyled | + kPropDynamic, inlineFixBackground}, + {kPropertyBackground, Color(), asFill, kPropOut | kPropVisualHash}, {kPropertyBorderRadii, Radii(), nullptr, kPropOut | kPropVisualHash}, @@ -186,7 +223,7 @@ FrameComponent::constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) layer->setChildClip(sg::path(outline.inset(borderWidth))); auto background = sg::draw(sg::path(outline.inset(strokeWidth)), - sg::fill(sg::paint(getCalculated(kPropertyBackgroundColor)))); + sg::fill(sg::paint(getCalculated(kPropertyBackground)))); auto border = sg::draw(sg::path(outline, strokeWidth), sg::fill(sg::paint(getCalculated(kPropertyBorderColor)))); @@ -204,7 +241,7 @@ FrameComponent::updateSceneGraphInternal(sg::SceneGraphUpdates& sceneGraph) auto borderWidthChanged = isDirty(kPropertyBorderWidth); auto drawnBorderWidthChanged = isDirty(kPropertyDrawnBorderWidth); - auto backgroundChanged = isDirty(kPropertyBackgroundColor); + auto backgroundChanged = isDirty(kPropertyBackground); auto borderColorChanged = isDirty(kPropertyBorderColor); auto strokeWidthChanged = isDirty(kPropertyBorderStrokeWidth); @@ -246,8 +283,9 @@ FrameComponent::updateSceneGraphInternal(sg::SceneGraphUpdates& sceneGraph) if (backgroundChanged) { auto* fill = sg::FillPathOp::cast(draw->getOp()); - auto* paint = sg::ColorPaint::cast(fill->paint); - result |= paint->setColor(getCalculated(kPropertyBackgroundColor).getColor()); + fill->paint = sg::paint(getCalculated(kPropertyBackground)); + // With gradient OR color paint there are no easy way to know if it has changed. + result |= true; } } @@ -283,6 +321,9 @@ void FrameComponent::assignProperties(const ComponentPropDefSet& propDefSet) { CoreComponent::assignProperties(propDefSet); + fixBackgroundByColor(false); + fixBackground(false); + fixBorder(false); calculateDrawnBorder(false); } diff --git a/aplcore/src/component/hostcomponent.cpp b/aplcore/src/component/hostcomponent.cpp index a7392fa..5a67d44 100644 --- a/aplcore/src/component/hostcomponent.cpp +++ b/aplcore/src/component/hostcomponent.cpp @@ -19,20 +19,16 @@ #include #include -#include "apl/component/componentproperties.h" -#include "apl/component/corecomponent.h" #include "apl/component/yogaproperties.h" #include "apl/content/content.h" +#include "apl/content/viewport.h" +#include "apl/datasource/datasourceprovider.h" #include "apl/document/coredocumentcontext.h" #include "apl/embed/documentregistrar.h" #include "apl/engine/keyboardmanager.h" #include "apl/engine/layoutmanager.h" -#include "apl/engine/propdef.h" #include "apl/engine/sharedcontextdata.h" #include "apl/engine/tickscheduler.h" -#include "apl/primitives/dimension.h" -#include "apl/primitives/object.h" -#include "apl/primitives/rect.h" #include "apl/time/sequencer.h" namespace { @@ -55,6 +51,10 @@ defaultHeight(Component& component, const RootConfig& rootConfig) } // unnamed namespace +static const char *EMBED_REINFLATE_SEQUENCER_PREFIX = "REINFLATE_SEQUENCER_"; + +static const int DEFAULT_HOST_DIMENSION = 100; + namespace apl { CoreComponentPtr @@ -76,35 +76,96 @@ HostComponent::HostComponent(const ContextPtr& context, : ActionableComponent(context, std::move(properties), path) {} +std::shared_ptr +HostComponent::cast(const ComponentPtr& component) +{ + return component && (CoreComponent::cast(component)->getType() == kComponentTypeHost) + ? std::static_pointer_cast(component) : nullptr; +} + +ConfigurationChange +HostComponent::filterConfigurationChange(const ConfigurationChange& configurationChange, + const Metrics& metrics) const +{ + auto parentContext = mContext; + auto overrideContext = Context::createFromParent(parentContext); + + // Override environment and viewport + overrideContext->putConstant( + "environment", + std::make_shared( + configurationChange.mergeEnvironment( + overrideContext->opt("environment").getMap() + ) + ) + ); + overrideContext->putConstant( + "viewport", + makeViewport( + configurationChange.mergeMetrics(metrics), + configurationChange.theme() + ) + ); + + // Re-resolve environment, if any, and add to the change + auto env = getProperty(kPropertyEnvironment); + auto originalEnv = std::make_shared(); + auto resolvedEnv = std::make_shared(); + if (!env.empty() && env.isMap()) { + // Evaluate props in both original context and new context + for (const auto& customEnv : env.getMap()) { + originalEnv->emplace(customEnv.first, evaluate(*parentContext, customEnv.second)); + resolvedEnv->emplace(customEnv.first, evaluate(*overrideContext, customEnv.second)); + } + } + + auto result = configurationChange.embeddedDocumentChange(); + // If any changed - apply to the change + if (originalEnv != resolvedEnv) { + for (const auto& entity : *resolvedEnv) + result.environmentValue(entity.first, entity.second); + } + return result; +} + const ComponentPropDefSet& HostComponent::propDefSet() const { - static auto resetOnLoadOnFailFlag = [](Component& component) { + static auto sourcePropertyChanged = [](Component& component) { auto& host = ((HostComponent&)component); host.mOnLoadOnFailReported = false; host.mNeedToRequestDocument = true; + host.detachEmbedded(); host.releaseEmbedded(); host.requestEmbedded(); }; static ComponentPropDefSet sHostComponentProperties(ActionableComponent::propDefSet(), { - {kPropertyHeight, Dimension(100), asNonAutoDimension, kPropIn | kPropDynamic | kPropStyled, yn::setHeight, defaultHeight}, - {kPropertyWidth, Dimension(100), asNonAutoDimension, kPropIn | kPropDynamic | kPropStyled, yn::setWidth, defaultWidth}, - {kPropertyEnvironment, Object::EMPTY_MAP(), asAny, kPropIn}, - {kPropertyOnFail, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyOnLoad, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertySource, "", asUrlRequest, kPropRequired | kPropIn | kPropDynamic | kPropVisualHash | kPropEvaluated, resetOnLoadOnFailFlag}, - {kPropertyEmbeddedDocument, Object::NULL_OBJECT(), asAny, kPropDynamic | kPropRuntimeState}, + {kPropertyHeight, Dimension(DEFAULT_HOST_DIMENSION), asDimension, kPropIn | kPropDynamic | kPropStyled, yn::setHeight, defaultHeight}, + {kPropertyWidth, Dimension(DEFAULT_HOST_DIMENSION), asDimension, kPropIn | kPropDynamic | kPropStyled, yn::setWidth, defaultWidth}, + {kPropertyEnvironment, Object::EMPTY_MAP(), asAny, kPropIn}, + {kPropertyOnFail, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnLoad, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyParameters, Object::EMPTY_MAP(), asAny, kPropIn | kPropDynamic}, + {kPropertySource, "", asUrlRequest, kPropRequired | kPropIn | kPropDynamic | kPropVisualHash | kPropEvaluated, sourcePropertyChanged}, + {kPropertyEmbeddedDocument, Object::NULL_OBJECT(), asAny, kPropDynamic | kPropRuntimeState}, }); return sHostComponentProperties; } +void +HostComponent::refreshContent(const ContentPtr& content, const DocumentConfigPtr& documentConfig) const +{ + content->refresh(generateChildMetrics(), *generateChildConfig(documentConfig)); + resolvePendingParameters(content); +} + void HostComponent::requestEmbedded() { const auto source = URLRequest::asURLRequest(getProperty(kPropertySource)); - mRequest = EmbedRequest::create(source, mContext->documentContext()); + mRequest = EmbedRequest::create(source, mContext->documentContext(), shared_from_this()); if (auto oldEmbeddedId = getDocumentId()) { mNeedToRequestDocument = false; @@ -150,7 +211,7 @@ HostComponent::requestEmbedded() } DocumentContextPtr -HostComponent::onLoad(const EmbeddedRequestSuccessResponse&& response) +HostComponent::onLoad(EmbeddedRequestSuccessResponse&& response) { if (mOnLoadOnFailReported) return nullptr; @@ -172,16 +233,15 @@ HostComponent::onLoadHandler() } DocumentContextPtr -HostComponent::initializeEmbedded(const EmbeddedRequestSuccessResponse&& response) +HostComponent::initializeEmbedded(EmbeddedRequestSuccessResponse&& response) { resolvePendingParameters(response.content); auto embedded = CoreDocumentContext::create( - mContext, + mContext->getShared(), + generateChildMetrics(), response.content, - getProperty(kPropertyEnvironment), - getCalculated(kPropertyInnerBounds).get().getSize(), - response.documentConfig); + *generateChildConfig(response.documentConfig)); const URLRequest& url = response.request->getUrlRequest(); if (!embedded || !embedded->setup(nullptr)) { @@ -211,6 +271,24 @@ HostComponent::initializeEmbedded(const EmbeddedRequestSuccessResponse&& respons return embedded; } +void +HostComponent::finalizeReinflate(const CoreDocumentContextPtr& document) +{ + document->finishReinflate([&](){ + auto coreTop = CoreComponent::cast(document->topComponent()); + // Can't fail + if (insertChild(coreTop, 0, true)) { + mContext->layoutManager().setAsTopNode(coreTop); + mContext->layoutManager().requestLayout(coreTop, false); + + document->processOnMounts(); + mContext->getShared()->tickScheduler().processTickHandlers(document); + } else { + assert(false); + } + }, mReinflationState.first, mReinflationState.second); +} + void HostComponent::reinflate() { @@ -223,17 +301,40 @@ HostComponent::reinflate() mContext->layoutManager().removeAsTopNode(CoreComponent::cast(embedded->topComponent())); CoreComponent::removeChild(coreTop, false); - embedded->reinflate([&](){ - auto coreTop = CoreComponent::cast(embedded->topComponent()); - // Can't fail - assert(insertChild(coreTop, 0, true)); + auto change = embedded->activeChanges(); + auto metrics = change.mergeMetrics(embedded->currentMetrics()); + auto config = change.mergeRootConfig(embedded->currentConfig()); - mContext->layoutManager().setAsTopNode(coreTop); - mContext->layoutManager().requestLayout(coreTop, false); + auto preservedSequencers = std::map(); + auto startReinflateResult = embedded->startReinflate(preservedSequencers); - embedded->processOnMounts(); - mContext->getShared()->tickScheduler().processTickHandlers(embedded); - }); + if (!startReinflateResult.first) { + LOG(LogLevel::kError) << "Failed to prepare for reinflation of embedded document " << embeddedId; + return; + } + + mReinflationState = std::make_pair(startReinflateResult.second, preservedSequencers); + + // Resolving asynchronously, required to get extensions resolved. + embedded->content()->refresh(metrics, config); + + if (embedded->content()->isWaiting()) { + auto timers = std::static_pointer_cast(config.getTimeManager()); + auto action = Action::make(timers, [this, embedded](ActionRef ref) { + embedded->contextPtr()->pushEvent(Event(kEventTypeContentRefresh, shared_from_this(), ref)); + }); + + auto wrapped = Action::wrapWithCallback(timers, action, [this, embedded](bool, const ActionPtr& ptr) { + finalizeReinflate(embedded); + }); + + auto sequencer = EMBED_REINFLATE_SEQUENCER_PREFIX + getUniqueId(); + getContext()->sequencer().terminateSequencer(sequencer); + getContext()->sequencer().attachToSequencer(wrapped, sequencer); + } else { + // If content has not changed - reinflate instantly + finalizeReinflate(embedded); + } } bool @@ -247,22 +348,37 @@ HostComponent::includeChildrenInVisualContext() const } void -HostComponent::resolvePendingParameters(const ContentPtr& content) -{ - if (!content->isReady()) { - for (const auto& param : content->getPendingParameters()) { - auto it = mProperties.find(param); - if (it != mProperties.end()) { - auto parameter = it->second; - if (parameter.isString()) { - parameter = evaluate(*mContext, parameter); - } - content->addObjectData(param, parameter); - } - else { - CONSOLE(mContext) << "Missing value for parameter " << param; +HostComponent::resolvePendingParameters(const ContentPtr& content) const +{ + // If content is ready, there's nothing further to do + if (content->isReady()) { + return; + } + + const auto& explicitParameters = getCalculated(kPropertyParameters); + for (const auto& param : content->getPendingParameters()) { + // Pending parameters will be resolved to null if we have nothing better + Object data = Object::NULL_OBJECT(); + + if (!explicitParameters.empty()) { + // Read from explicit parameters provided + data = explicitParameters.get(param); + } else { + // When there are no explicit parameters, we allow implicit parameters that do not + // conflict with intrinsic properties. + const auto& intrinsicValue = mCalculated.get(param); + if (intrinsicValue.isNull()) { + auto it = mProperties.find(param); + if (it != mProperties.end()) { + data = it->second; + } + } else { + CONSOLE(mContext) << "Could not read intrinsic property " << param; } } + + data = evaluate(*mContext, data); + content->addObjectData(param, data); } } @@ -332,6 +448,97 @@ HostComponent::releaseEmbedded() mRequest = nullptr; } +RootConfigPtr +HostComponent::generateChildConfig(const DocumentConfigPtr& documentConfig) const +{ + auto env = getProperty(kPropertyEnvironment); + const RootConfig& rootConfig = mContext->getRootConfig(); + RootConfigPtr embeddedRootConfig = rootConfig.copy(); + embeddedRootConfig->set(RootProperty::kLang, env.opt("lang", mContext->getLang())); + embeddedRootConfig->set(RootProperty::kLayoutDirection, + env.opt("layoutDirection", sLayoutDirectionMap.get(mContext->getLayoutDirection(), ""))); + + embeddedRootConfig->set(RootProperty::kAllowOpenUrl, + rootConfig.getProperty(RootProperty::kAllowOpenUrl).getBoolean() + && env.opt("allowOpenURL", true).asBoolean()); + + auto copyDisallowProp = [&]( RootProperty propName, const std::string& envName ) { + embeddedRootConfig->set(propName, rootConfig.getProperty(propName).getBoolean() || env.opt(envName, false).asBoolean()); + }; + + copyDisallowProp(RootProperty::kDisallowDialog, "disallowDialog"); + copyDisallowProp(RootProperty::kDisallowEditText, "disallowEditText"); + copyDisallowProp(RootProperty::kDisallowVideo, "disallowVideo"); + + if (documentConfig != nullptr) { +#ifdef ALEXAEXTENSIONS + embeddedRootConfig->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider); + embeddedRootConfig->extensionMediator(documentConfig->getExtensionMediator()); +#endif + + for (const auto& provider : documentConfig->getDataSourceProviders()) { + embeddedRootConfig->dataSourceProvider(provider->getType(), provider); + } + + for (const auto& ev : documentConfig->getEnvironmentValues()) { + embeddedRootConfig->setEnvironmentValue(ev.first, ev.second); + } + } + + // Any custom env props allowed. Config does check internally. Values are evaluated. + if (env.isMap()) { + for (const auto& customEnv : env.getMap()) { + embeddedRootConfig->setEnvironmentValue(customEnv.first, evaluate(*mContext, customEnv.second)); + } + } + + return embeddedRootConfig; +} + +bool +HostComponent::isAutoWidth() const +{ + return getCalculated(kPropertyWidth).isAutoDimension(); +} + +bool +HostComponent::isAutoHeight() const +{ + return getCalculated(kPropertyHeight).isAutoDimension(); +} + +Metrics +HostComponent::generateChildMetrics() const +{ + auto size = getCalculated(kPropertyInnerBounds).get().getSize(); + + // std::lround use copied from Dimension::AbsoluteDimensionObjectType::asInt + int width = (int) std::lround(mContext->dpToPx(size.getWidth())); + int height = (int) std::lround(mContext->dpToPx(size.getHeight())); + + auto result = Metrics(); + + // If auto - metrics only used for context, so don't matter what value they have. Auto will get + // reported as true. Set target size to default. + if (isAutoWidth()) { + if (width == 0) width = DEFAULT_HOST_DIMENSION; + auto minmax = mContext->layoutManager().getMinMaxWidth(*this); + result.minAndMaxWidth((int)minmax.first, (int)minmax.second); + } + if (isAutoHeight()) { + if (height == 0) height = DEFAULT_HOST_DIMENSION; + auto minmax = mContext->layoutManager().getMinMaxHeight(*this); + result.minAndMaxHeight((int)minmax.first, (int)minmax.second); + } + + return result + .size(width, height) + .shape(mContext->getScreenShape()) + .theme(mContext->getTheme().c_str()) + .dpi(mContext->getDpi()) + .mode(mContext->getViewportMode()); +} + void HostComponent::setDocument(int id, bool connectedVC) { @@ -374,6 +581,20 @@ HostComponent::getVisualContextType() const : VISUAL_CONTEXT_TYPE_EMPTY; } +bool +HostComponent::getTags(rapidjson::Value& outMap, rapidjson::Document::AllocatorType& allocator) { + CoreComponent::getTags(outMap, allocator); + + rapidjson::Value embedded(rapidjson::kObjectType); + + embedded.AddMember("source", rapidjson::Value(URLRequest::asURLRequest(getProperty(kPropertySource)).getUrl().c_str(), allocator).Move(), allocator); + embedded.AddMember("attached", includeChildrenInVisualContext(), allocator); + + outMap.AddMember("embedded", embedded, allocator); + + return true; +} + bool HostComponent::executeKeyHandlers(KeyHandlerType type, const Keyboard& keyboard) { @@ -398,7 +619,9 @@ HostComponent::processLayoutChanges(bool useDirtyFlag, bool first) CoreComponent::processLayoutChanges(useDirtyFlag, first); auto embeddedId = getDocumentId(); - if (!embeddedId || mNeedToRequestDocument) return; + + // If Host autosized - do not perform config change based on size, just replace the size + if (!embeddedId || mNeedToRequestDocument || isAutoHeight() || isAutoWidth()) return; auto boundsAfterLayout = getProperty(kPropertyInnerBounds); auto embedded = mContext->getShared()->documentRegistrar().get(embeddedId); @@ -406,8 +629,9 @@ HostComponent::processLayoutChanges(bool useDirtyFlag, bool first) auto size = boundsAfterLayout.get(); embedded->configurationChange(ConfigurationChange( // std::lround use copied from Dimension::AbsoluteDimensionObjectType::asInt - std::lround(mContext->dpToPx(size.getWidth())), - std::lround(mContext->dpToPx(size.getHeight())))); + (int) std::lround(mContext->dpToPx(size.getWidth())), + (int) std::lround(mContext->dpToPx(size.getHeight()))), + false); } } diff --git a/aplcore/src/component/imagecomponent.cpp b/aplcore/src/component/imagecomponent.cpp index 35e4174..18e8fd2 100644 --- a/aplcore/src/component/imagecomponent.cpp +++ b/aplcore/src/component/imagecomponent.cpp @@ -136,6 +136,8 @@ ImageComponent::constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) auto layer = CoreComponent::constructSceneGraphLayer(sceneGraph); assert(layer); + layer->setCharacteristic(sg::Layer::kCharacteristicHasMedia); + auto filterPtr = getFilteredImage(); auto rects = getImageRects(filterPtr); diff --git a/aplcore/src/component/multichildscrollablecomponent.cpp b/aplcore/src/component/multichildscrollablecomponent.cpp index b3088d5..1b82185 100644 --- a/aplcore/src/component/multichildscrollablecomponent.cpp +++ b/aplcore/src/component/multichildscrollablecomponent.cpp @@ -504,7 +504,7 @@ MultiChildScrollableComponent::trimScroll(const Point& point) if (mAvailableRange.contains(isVertical() ? point.getY() : point.getX())) return point; // Break out early. If there are no children - no scrolling possible - if (shouldNotPropagateLayoutChanges() || + if (!shouldPropagateLayoutChanges() || getCalculated(kPropertyBounds).empty()) return Point(); @@ -889,6 +889,7 @@ MultiChildScrollableComponent::scheduleDelayedLayout() { if (self) { self->processLayoutChangesInternal(true, false, true, false); self->mDelayLayoutAction = nullptr; + self->markAccessibilityDirty(); } }); } @@ -928,7 +929,7 @@ MultiChildScrollableComponent::processLayoutChangesInternal(bool useDirtyFlag, b mRebuilder->notifyEndEdgeReached(); } - if (shouldNotPropagateLayoutChanges()) { + if (!shouldPropagateLayoutChanges()) { // Starting with empty or invalid sequence return; } @@ -968,19 +969,19 @@ MultiChildScrollableComponent::processLayoutChangesInternal(bool useDirtyFlag, b } const auto& sequenceBounds = mCalculated.get(kPropertyBounds).get(); - float childCache = mContext->getRootConfig().getSequenceChildCache(); + float childCache = mContext->getRootConfig().getProperty(RootProperty::kSequenceChildCache).getDouble(); float pageSize = horizontal ? sequenceBounds.getWidth() : sequenceBounds.getHeight(); // Try to figure majority of layout as a bulk // // TODO: Layout heuristics are good for performance but not essential. In - // an earlier version, the heuristic looked at the size of the first child - // to estimate how many children need to be laid out. In a later version we - // looked at the second child instead, to avoid cases where a narrow first - // child resulted in over-estimation of the number of children that needed - // to be laid out. This change had unintended consequences for certain - // layouts that counted on the original heuristic. We need to re-engineer - // the heuristic and in the mean time, we can disable it. + // an earlier version, the heuristic looked at the size of the first child + // to estimate how many children need to be laid out. In a later version we + // looked at the second child instead, to avoid cases where a narrow first + // child resulted in over-estimation of the number of children that needed + // to be laid out. This change had unintended consequences for certain + // layouts that counted on the original heuristic. We need to re-engineer + // the heuristic and in the mean time, we can disable it. // // runLayoutHeuristics(anchorIdx, childCache, pageSize, useDirtyFlag, first); // diff --git a/aplcore/src/component/pagercomponent.cpp b/aplcore/src/component/pagercomponent.cpp index be31bea..240c76d 100644 --- a/aplcore/src/component/pagercomponent.cpp +++ b/aplcore/src/component/pagercomponent.cpp @@ -22,7 +22,7 @@ #include "apl/focus/focusmanager.h" #include "apl/livedata/layoutrebuilder.h" #include "apl/livedata/livearrayobject.h" -#include "apl/primitives/keyboard.h" +#include "apl/primitives/accessibilityaction.h" #include "apl/time/sequencer.h" #include "apl/time/timemanager.h" #include "apl/touch/gestures/pagerflinggesture.h" @@ -63,6 +63,60 @@ PagerComponent::clearActiveStateSelf() mDelayLayoutAction = nullptr; } +void +PagerComponent::getSupportedStandardAccessibilityActions(std::map& result) const +{ + ActionableComponent::getSupportedStandardAccessibilityActions(result); + if (getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureDynamicAccessibilityActions)) { + auto availableDirection = pageDirection(); + switch (availableDirection) { + case kPageDirectionNone: + break; + case kPageDirectionForward: + result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, true); + break; + case kPageDirectionBack: + result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, true); + break; + case kPageDirectionBoth: + result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, true); + result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, true); + break; + } + } else { + result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, true); + result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, true); + } +} + +void +PagerComponent::invokeStandardAccessibilityAction(const std::string& name) +{ + bool pageChange = false; + if (name == AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD) { + setPageImmediate(kPageDirectionForward); + pageChange = true; + } else if (name == AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD) { + setPageImmediate(kPageDirectionBack); + pageChange = true; + } else + ActionableComponent::invokeStandardAccessibilityAction(name); + + if (pageChange && + getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureDynamicAccessibilityActions)) { + // If we have focus set to a CHILD of pager it should go to the next child, or to pager if + // no such. + auto focused = getContext()->focusManager().getFocus(); + auto self = shared_from_corecomponent(); + if (focused && focused != self && isParentOf(focused)) { + auto next = getContext()->focusManager().find(kFocusDirectionForward, nullptr, Rect(), self); + if (!next) next = self; + + getContext()->focusManager().setFocus(next, true, false); + } + } +} + inline Object defaultWidth(Component& component, const RootConfig& rootConfig) { return rootConfig.getDefaultComponentWidth(component.getType()); @@ -117,7 +171,7 @@ PagerComponent::propDefSet() const { {kPropertyPageDirection, kScrollDirectionHorizontal, sScrollDirectionMap, kPropIn | kPropDynamic}, {kPropertyHandlePageMove, Object::EMPTY_ARRAY(), asArray, kPropIn}, {kPropertyOnPageChanged, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyCurrentPage, 0, asInteger, kPropRuntimeState | kPropVisualContext}, + {kPropertyCurrentPage, 0, asInteger, kPropRuntimeState | kPropVisualContext | kPropAccessibility}, {kPropertyPageId, getPageId, setPageId, kPropDynamic }, {kPropertyPageIndex, getPageIndex, setPageIndex, kPropDynamic }, }); @@ -126,7 +180,7 @@ PagerComponent::propDefSet() const { } std::shared_ptr -PagerComponent::cast(const std::shared_ptr& component) { +PagerComponent::cast(const ComponentPtr& component) { return component && component->getType() == ComponentType::kComponentTypePager ? std::static_pointer_cast(component) : nullptr; } @@ -149,8 +203,8 @@ PagerComponent::update(UpdateType type, float value) { // Update is used only in case if change of the page was performed by viewhost implementation (non-native gesture // processing). Animations is up to viewhost so we only update core internal state. int currentPage = pagePosition(); - if (value != currentPage) { - setPage(value); + if (static_cast(value) != currentPage) { + setPage(static_cast(value)); markDisplayedChildrenStale(true); executePageChangeEvent(type == kUpdatePagerByEvent); } @@ -186,25 +240,35 @@ PagerComponent::setPageImmediate(int pageIndex) if (allowEventHandlers()) executePageChangeEvent(true); + + markDisplayedChildrenStale(true); } void PagerComponent::setPageUtil( - const apl::ContextPtr& context, const apl::ComponentPtr& target, int index, PageDirection direction, const ActionRef& ref, - bool skipDefaultAnimation) + bool skipDefaultAnimation, + apl_duration_t transitionDuration) { - if (target->getType() == ComponentType::kComponentTypePager) { - std::static_pointer_cast(target)->handleSetPage(index, direction, ref, - skipDefaultAnimation); - } + auto pager = PagerComponent::cast(target); + if (pager) + pager->handleSetPage(index, direction, ref, skipDefaultAnimation, transitionDuration); +} + +void +PagerComponent::setPageImmediate(PageDirection direction) +{ + auto index = direction == kPageDirectionForward ? pagePosition() + 1 : pagePosition() - 1; + auto pages = static_cast(getChildCount()); + int result = index % pages; + setPageImmediate(result >= 0 ? result : result + pages); } void -PagerComponent::handleSetPage(int index, PageDirection direction, const ActionRef& ref, bool skipDefaultAnimation) +PagerComponent::handleSetPage(int index, PageDirection direction, const ActionRef& ref, bool skipDefaultAnimation, apl_duration_t transitionDuration) { auto currentPage = pagePosition(); @@ -226,20 +290,27 @@ PagerComponent::handleSetPage(int index, PageDirection direction, const ActionRe // Set initial state startPageMove(direction, currentPage, index); + if (!mPageMoveHandler) { + // Created in the previous step, should not happen + assert(false); + return; + } + // Animate if required. std::weak_ptr weak_ptr(std::static_pointer_cast(shared_from_this())); disableGestures(); - if (mPageMoveHandler && !(mPageMoveHandler->isDefault() && skipDefaultAnimation)) { - auto duration = getRootConfig().getDefaultPagerAnimationDuration(); + + // We skip animation if asked to do so, on default handler and duration was not explicitly set + if (mPageMoveHandler->isDefault() && skipDefaultAnimation && transitionDuration <= 0) { + mCurrentAnimation = Action::makeDelayed(getRootConfig().getTimeManager(), 0); + } else { + if (transitionDuration < 0) + transitionDuration = getRootConfig().getProperty(RootProperty::kDefaultPagerAnimationDuration).getDouble(); mCurrentAnimation = Action::makeAnimation(getRootConfig().getTimeManager(), - duration, [weak_ptr, duration](apl_duration_t offset){ + transitionDuration, [weak_ptr, transitionDuration](apl_duration_t offset){ auto self = weak_ptr.lock(); - if (self) { - self->executePageMove(offset/duration); - } - }); - } else { - mCurrentAnimation = Action::makeDelayed(getRootConfig().getTimeManager(), 0); + if (self) self->executePageMove(offset/transitionDuration); + }); } mCurrentAnimation->then([weak_ptr, ref](const ActionPtr& actionPtr){ @@ -688,6 +759,7 @@ PagerComponent::finalizePopulate() } } } + markAccessibilityDirty(); } void @@ -706,7 +778,7 @@ PagerComponent::attachPageAndReportLoaded(int page) { * in case the next page needs to lay out complicated text blocks. */ const auto childCount = static_cast(mChildren.size()); - const auto pagerChildCache = mContext->getRootConfig().getPagerChildCache(); + const auto pagerChildCache = mContext->getRootConfig().getProperty(RootProperty::kPagerChildCache).getInteger(); const auto navigation = static_cast(getCalculated(kPropertyNavigation).getInteger()); int start = 0; @@ -815,7 +887,7 @@ PagerComponent::takeFocusFromChild(FocusDirection direction, const Rect& origin) const auto targetPage = (int)((pagePosition() + delta + childCount) % childCount); // Reset any running commands that may affect page position. getContext()->sequencer().releaseResource({kExecutionResourcePosition, shared_from_this()}); - setPageUtil(getContext(), shared_from_corecomponent(), targetPage, targetDirection, ActionRef(nullptr)); + setPageUtil(shared_from_corecomponent(), targetPage, targetDirection, ActionRef(nullptr)); // Need to have that to base search on what it will be. setPage(targetPage); auto pager = shared_from_corecomponent(); diff --git a/aplcore/src/component/scrollablecomponent.cpp b/aplcore/src/component/scrollablecomponent.cpp index 5bc280e..b209621 100644 --- a/aplcore/src/component/scrollablecomponent.cpp +++ b/aplcore/src/component/scrollablecomponent.cpp @@ -20,6 +20,7 @@ #include "apl/component/yogaproperties.h" #include "apl/content/rootconfig.h" #include "apl/focus/focusmanager.h" +#include "apl/primitives/accessibilityaction.h" #include "apl/time/sequencer.h" #include "apl/time/timemanager.h" #include "apl/touch/gestures/scrollgesture.h" @@ -34,14 +35,89 @@ namespace apl { ScrollableComponent::ScrollableComponent(const ContextPtr& context, Properties&& properties, const Path& path) : ActionableComponent(context, std::move(properties), path), - mStickyTree(std::make_shared(*this)) {} + mStickyTree(std::make_shared(*this)) { + YGNodeStyleSetOverflow(mYGNodeRef, YGOverflowScroll); +} std::shared_ptr -ScrollableComponent::cast(const std::shared_ptr& component) { +ScrollableComponent::cast(const ComponentPtr& component) { return component && CoreComponent::cast(component)->scrollable() ? std::static_pointer_cast(component) : nullptr; } +void +ScrollableComponent::getSupportedStandardAccessibilityActions(std::map& result) const +{ + ActionableComponent::getSupportedStandardAccessibilityActions(result); + if (getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureDynamicAccessibilityActions)) { + if (allowForward()) result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, true); + if (allowBackwards()) result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, true); + } else { + result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, true); + result.emplace(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, true); + } +} + +void +ScrollableComponent::scroll(bool backwards) +{ + auto innerBounds = getCalculated(kPropertyInnerBounds).get(); + auto distance = (isVertical() ? innerBounds.getHeight() : innerBounds.getWidth()); + + if (backwards) + distance *= -1.0f; + if (!isVertical() && (getCalculated(kPropertyLayoutDirection) == kLayoutDirectionRTL)) + distance *= -1.0f; + + // Calculate the new position by trimming the old position plus the distance + auto position = trimScroll(scrollPosition() + Point(distance, distance)); + setScrollPositionDirectly(isVertical() ? position.getY() : position.getX()); +} + +void +ScrollableComponent::invokeStandardAccessibilityAction(const std::string& name) +{ + auto direction = kFocusDirectionNone; + if (name == AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD) { + scroll(false); + direction = kFocusDirectionForward; + } else if (name == AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD) { + scroll(true); + direction = kFocusDirectionBackwards; + } else + ActionableComponent::invokeStandardAccessibilityAction(name); + + if (direction != kFocusDirectionNone && + getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureDynamicAccessibilityActions)) { + // If we have focus set to a CHILD of scrollable it should go to the next child, or to self + // if no such. + auto focused = getContext()->focusManager().getFocus(); + auto self = shared_from_corecomponent(); + if (focused && focused != self && isParentOf(focused)) { + auto next = getContext()->focusManager() + .find(direction, + focused, + focused->getCalculated(kPropertyBounds).get(), + self); + if (next) { + // If New focused element is on the current scrolling page - just focus it, if not - + // focus scrollable itself + Rect bounds; + next->getBoundsInParent(self, bounds); + bounds.offset(-scrollPosition()); + + auto parentBound = getCalculated(kPropertyInnerBounds).get(); + + if (bounds.intersect(parentBound).empty()) + next = self; + } else { + next = self; + } + getContext()->focusManager().setFocus(next, true, false); + } + } +} + const ComponentPropDefSet& ScrollableComponent::propDefSet() const { @@ -70,7 +146,7 @@ ScrollableComponent::propDefSet() const static ComponentPropDefSet sScrollableComponentProperties(ActionableComponent::propDefSet(), { {kPropertyScrollOffset, getScrollOffset, setScrollOffset, kPropDynamic | kPropSetAfterLayout }, {kPropertyScrollPercent, getScrollPercent, setScrollPercent, kPropDynamic | kPropSetAfterLayout }, - {kPropertyScrollPosition, Dimension(0), asAbsoluteDimension, kPropRuntimeState | kPropVisualContext}, + {kPropertyScrollPosition, Dimension(0), asAbsoluteDimension, kPropRuntimeState | kPropVisualContext | kPropAccessibility}, }); return sScrollableComponentProperties; } diff --git a/aplcore/src/component/scrollviewcomponent.cpp b/aplcore/src/component/scrollviewcomponent.cpp index 437656d..7777297 100644 --- a/aplcore/src/component/scrollviewcomponent.cpp +++ b/aplcore/src/component/scrollviewcomponent.cpp @@ -35,9 +35,7 @@ ScrollViewComponent::create(const ContextPtr& context, ScrollViewComponent::ScrollViewComponent(const ContextPtr& context, Properties&& properties, const Path& path) - : ScrollableComponent(context, std::move(properties), path) { - YGNodeStyleSetOverflow(mYGNodeRef, YGOverflowScroll); -} + : ScrollableComponent(context, std::move(properties), path) {} const ComponentPropDefSet& ScrollViewComponent::propDefSet() const { diff --git a/aplcore/src/component/textcomponent.cpp b/aplcore/src/component/textcomponent.cpp index b629496..e4edbcf 100644 --- a/aplcore/src/component/textcomponent.cpp +++ b/aplcore/src/component/textcomponent.cpp @@ -80,7 +80,7 @@ const ComponentPropDefSet& TextComponent::propDefSet() const { static auto defaultFontFamily = [](Component& component, const RootConfig& rootConfig) -> Object { - return Object(rootConfig.getDefaultFontFamily()); + return rootConfig.getProperty(RootProperty::kDefaultFontFamily); }; static auto defaultFontColor = [](Component& component, const RootConfig& rootConfig) -> Object { diff --git a/aplcore/src/component/touchablecomponent.cpp b/aplcore/src/component/touchablecomponent.cpp index d3293dd..6a6522e 100644 --- a/aplcore/src/component/touchablecomponent.cpp +++ b/aplcore/src/component/touchablecomponent.cpp @@ -18,7 +18,7 @@ #include "apl/component/componentpropdef.h" #include "apl/content/rootconfig.h" #include "apl/engine/keyboardmanager.h" -#include "apl/engine/propdef.h" +#include "apl/primitives/accessibilityaction.h" #include "apl/time/sequencer.h" #include "apl/touch/gesture.h" @@ -40,6 +40,16 @@ static const std::map sPropertyExecutesFast = { {kPropertyOnUp, true} }; +void +TouchableComponent::getSupportedStandardAccessibilityActions(std::map& result) const +{ + ActionableComponent::getSupportedStandardAccessibilityActions(result); + auto onPressHandler = getCalculated(kPropertyOnPress); + if (!onPressHandler.empty()) { + result.emplace("activate", false); + } +} + void TouchableComponent::setGestureHandlers() { @@ -68,7 +78,7 @@ void TouchableComponent::invokeStandardAccessibilityAction(const std::string& name) { auto onPressHandler = getCalculated(kPropertyOnPress); - if (name == "activate" && !onPressHandler.empty()) + if (name == AccessibilityAction::ACCESSIBILITY_ACTION_ACTIVATE && !onPressHandler.empty()) executeEventHandler(sPropertyHandlers.at(kPropertyOnPress), onPressHandler, false, createTouchEventProperties(Point())); else diff --git a/aplcore/src/component/touchwrappercomponent.cpp b/aplcore/src/component/touchwrappercomponent.cpp index c6f1e2f..ae3961e 100644 --- a/aplcore/src/component/touchwrappercomponent.cpp +++ b/aplcore/src/component/touchwrappercomponent.cpp @@ -38,7 +38,7 @@ TouchWrapperComponent::TouchWrapperComponent(const ContextPtr& context, } std::shared_ptr -TouchWrapperComponent::cast(const std::shared_ptr& component) { +TouchWrapperComponent::cast(const ComponentPtr& component) { return component && component->getType() == ComponentType::kComponentTypeTouchWrapper ? std::static_pointer_cast(component) : nullptr; } diff --git a/aplcore/src/component/vectorgraphiccomponent.cpp b/aplcore/src/component/vectorgraphiccomponent.cpp index ecb5845..75b8fbd 100644 --- a/aplcore/src/component/vectorgraphiccomponent.cpp +++ b/aplcore/src/component/vectorgraphiccomponent.cpp @@ -79,6 +79,7 @@ VectorGraphicComponent::propDefSet() const {kPropertyMediaBounds, Object::NULL_OBJECT(), nullptr, kPropOut | kPropVisualHash}, {kPropertyOnFail, Object::EMPTY_ARRAY(), asCommand, kPropIn}, {kPropertyOnLoad, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyParameters, Object::EMPTY_MAP(), asAny, kPropIn | kPropDynamic}, {kPropertyScale, kVectorGraphicScaleNone, sVectorGraphicScaleMap, kPropInOut | kPropStyled | kPropDynamic | kPropVisualHash, checkLayout}, {kPropertySource, "", asVectorGraphicSource, kPropInOut | kPropDynamic | kPropVisualHash | kPropEvaluated, resetMediaState}, }); @@ -102,7 +103,7 @@ VectorGraphicComponent::initialize() if (!graphicResource.empty()) { auto graphic = Graphic::create(mContext, graphicResource, - Properties(mProperties), + getGraphicParameters(), shared_from_corecomponent()); if (graphic) { mCalculated.set(kPropertyGraphic, graphic); @@ -166,7 +167,7 @@ VectorGraphicComponent::updateGraphic(const GraphicContentPtr& json) auto path = Path("_url").addObject(url); auto g = Graphic::create(mContext, json->get(), - Properties(mProperties), + getGraphicParameters(), shared_from_corecomponent(), path, getStyle()); @@ -209,7 +210,7 @@ VectorGraphicComponent::sourcePropertyChanged() if (!graphicResource.empty()) { auto graphic = Graphic::create(mContext, graphicResource, - Properties(mProperties), + getGraphicParameters(), shared_from_corecomponent()); if (graphic) { mCalculated.set(kPropertyGraphic, graphic); diff --git a/aplcore/src/component/videocomponent.cpp b/aplcore/src/component/videocomponent.cpp index 15bc63d..a9506c6 100644 --- a/aplcore/src/component/videocomponent.cpp +++ b/aplcore/src/component/videocomponent.cpp @@ -16,6 +16,7 @@ #include "apl/component/videocomponent.h" #include "apl/component/componentpropdef.h" +#include "apl/content/rootconfig.h" #include "apl/component/yogaproperties.h" #include "apl/media/mediautils.h" #include "apl/primitives/mediasource.h" @@ -62,7 +63,7 @@ VideoComponent::create(const ContextPtr& context, } std::shared_ptr -VideoComponent::cast(const std::shared_ptr& component) { +VideoComponent::cast(const ComponentPtr& component) { return component && component->getType() == ComponentType::kComponentTypeVideo ? std::static_pointer_cast(component) : nullptr; } @@ -173,10 +174,14 @@ VideoComponent::VideoComponent(const ContextPtr& context, : CoreComponent(context, std::move(properties), path), mMediaSequencer("VIDEO"+getUniqueId()) { - mMediaPlayer = mContext->mediaPlayerFactory().createPlayer( - [this] (MediaPlayerEventType eventType, const MediaState& mediaState) { - playerCallback(eventType, mediaState); - }); + mIsDisallowed = context->getRootConfig().getProperty(RootProperty::kDisallowVideo).asBoolean(); + + if (!mIsDisallowed) { + mMediaPlayer = mContext->mediaPlayerFactory().createPlayer( + [this] (MediaPlayerEventType eventType, const MediaState& mediaState) { + playerCallback(eventType, mediaState); + }); + } } VideoComponent::~VideoComponent() noexcept @@ -566,6 +571,8 @@ VideoComponent::constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) auto top = CoreComponent::constructSceneGraphLayer(sceneGraph); assert(top); + top->setCharacteristic(sg::Layer::kCharacteristicHasMedia); + // Find the target bounding box of the image. This is where we will be drawing the image. This is in DP. const auto& innerBounds = getCalculated(kPropertyInnerBounds).get(); auto scale = static_cast(getCalculated(kPropertyScale).getInteger()); diff --git a/aplcore/src/content/aplversion.cpp b/aplcore/src/content/aplversion.cpp index 56b29a6..b941023 100644 --- a/aplcore/src/content/aplversion.cpp +++ b/aplcore/src/content/aplversion.cpp @@ -35,6 +35,7 @@ static const Bimap sVersionMap = { { APLVersion::kAPLVersion20222, "2022.2" }, { APLVersion::kAPLVersion20231, "2023.1" }, { APLVersion::kAPLVersion20232, "2023.2" }, + { APLVersion::kAPLVersion20233, "2023.3" }, }; bool diff --git a/aplcore/src/content/configurationchange.cpp b/aplcore/src/content/configurationchange.cpp index 7fca597..f3d600c 100644 --- a/aplcore/src/content/configurationchange.cpp +++ b/aplcore/src/content/configurationchange.cpp @@ -56,14 +56,41 @@ ConfigurationChange::mergeRootConfig(const RootConfig& oldRootConfig) const if ((mFlags & kConfigurationChangeEnvironment) != 0) { for (const auto &prop : mEnvironment) { - if (rootConfig.getEnvironmentValues().count(prop.first) > 0) - rootConfig.setEnvironmentValue(prop.first, prop.second); + rootConfig.setEnvironmentValue(prop.first, prop.second); } } return rootConfig; } +ObjectMap +ConfigurationChange::mergeEnvironment(const ObjectMap& oldEnvironment) const +{ + auto environment = oldEnvironment; + + environment["reason"] = "reinflation"; + + if ((mFlags & kConfigurationChangeScreenMode) != 0) + environment["screenMode"] = mScreenMode; + + if ((mFlags & kConfigurationChangeFontScale) != 0) + environment["fontScale"] = mFontScale; + + if ((mFlags & kConfigurationChangeScreenReader) != 0) + environment["screenReader"] = mScreenReaderEnabled; + + if ((mFlags & kConfigurationChangeDisallowVideo) != 0) + environment["disallowVideo"] = mDisallowVideo; + + if ((mFlags & kConfigurationChangeEnvironment) != 0) { + for (const auto &prop : mEnvironment) { + environment[prop.first] = prop.second; + } + } + + return environment; +} + void ConfigurationChange::mergeConfigurationChange(const ConfigurationChange& other) { @@ -72,6 +99,10 @@ ConfigurationChange::mergeConfigurationChange(const ConfigurationChange& other) if ((other.mFlags & kConfigurationChangeSize) != 0) { mPixelWidth = other.mPixelWidth; mPixelHeight = other.mPixelHeight; + mMinPixelWidth = other.mMinPixelWidth; + mMinPixelHeight = other.mMinPixelHeight; + mMaxPixelWidth = other.mMaxPixelWidth; + mMaxPixelHeight = other.mMaxPixelHeight; } if ((other.mFlags & kConfigurationChangeTheme) != 0) @@ -99,7 +130,8 @@ ConfigurationChange::mergeConfigurationChange(const ConfigurationChange& other) } ConfigurationChange& -ConfigurationChange::environmentValue(const std::string &name, const Object &newValue) { +ConfigurationChange::environmentValue(const std::string &name, const Object &newValue) +{ mFlags |= kConfigurationChangeEnvironment; mEnvironment[name] = newValue; return *this; @@ -140,8 +172,23 @@ ConfigurationChange::asEventProperties(const RootConfig& rootConfig, const Metri }; } +ConfigurationChange +ConfigurationChange::embeddedDocumentChange() const +{ + // Only specific parameters allowed to propagate to embedded change. Everything else is Host-driven. + auto embeddedConfigChange = ConfigurationChange(); + if ((mFlags & kConfigurationChangeTheme) != 0) embeddedConfigChange.theme(mTheme.c_str()); + if ((mFlags & kConfigurationChangeViewportMode) != 0) embeddedConfigChange.mode(mViewportMode); + if ((mFlags & kConfigurationChangeFontScale) != 0) embeddedConfigChange.fontScale(mFontScale); + if ((mFlags & kConfigurationChangeScreenMode) != 0) embeddedConfigChange.screenMode(mScreenMode); + if ((mFlags & kConfigurationChangeScreenReader) != 0) embeddedConfigChange.screenReader(mScreenReaderEnabled); + + return embeddedConfigChange; +} + const std::set & -ConfigurationChange::getSynthesizedPropertyNames() { +ConfigurationChange::getSynthesizedPropertyNames() +{ static std::set sNames = { "rotated", "sizeChanged" diff --git a/aplcore/src/content/content.cpp b/aplcore/src/content/content.cpp index 1417bd2..f210f4f 100644 --- a/aplcore/src/content/content.cpp +++ b/aplcore/src/content/content.cpp @@ -19,12 +19,14 @@ #include "apl/buildTimeConstants.h" #include "apl/command/arraycommand.h" +#include "apl/component/hostcomponent.h" #include "apl/content/extensionrequest.h" #include "apl/content/importrequest.h" #include "apl/content/jsondata.h" #include "apl/content/metrics.h" #include "apl/content/package.h" #include "apl/content/settings.h" +#include "apl/embed/embedrequest.h" #include "apl/engine/arrayify.h" #include "apl/engine/parameterarray.h" #include "apl/engine/propdef.h" @@ -41,6 +43,12 @@ const char* DOCUMENT_MAIN_TEMPLATE = "mainTemplate"; const char* DOCUMENT_ENVIRONMENT = "environment"; const char* DOCUMENT_LANGUAGE = "lang"; const char* DOCUMENT_LAYOUT_DIRECTION = "layoutDirection"; +const char* PACKAGE_TYPE = "type"; +const char* PACKAGE_TYPE_PACKAGE = "package"; +const char* PACKAGE_TYPE_ONEOF = "oneOf"; +const char* PACKAGE_TYPE_ALLOF = "allOf"; +const char* PACKAGE_OTHERWISE = "otherwise"; +const char* PACKAGE_ITEMS = "items"; ContentPtr Content::create(JsonData&& document) @@ -50,6 +58,21 @@ Content::create(JsonData&& document) ContentPtr Content::create(JsonData&& document, const SessionPtr& session) +{ + return create(std::move(document), session, Metrics(), RootConfig(), false); +} + + +ContentPtr +Content::create(JsonData&& document, const SessionPtr& session, + const Metrics& metrics, const RootConfig& config) +{ + return create(std::move(document), session, metrics, config, true); +} + +ContentPtr +Content::create(JsonData&& document, const SessionPtr& session, const Metrics& metrics, + const RootConfig& config, bool supportsEvaluation) { if (!document) { CONSOLE(session).log("Document parse error offset=%u: %s.", document.offset(), document.error()); @@ -67,17 +90,75 @@ Content::create(JsonData&& document, const SessionPtr& session) return nullptr; } - return std::make_shared(session, ptr, it->value); + auto result = std::make_shared(session, ptr, it->value, metrics, config); + result->init(supportsEvaluation); + return result; } Content::Content(const SessionPtr& session, const PackagePtr& mainPackagePtr, - const rapidjson::Value& mainTemplate) + const rapidjson::Value& mainTemplate, + const Metrics& metrics, + const RootConfig& rootConfig) : mSession(session), mMainPackage(mainPackagePtr), mState(LOADING), - mMainTemplate(mainTemplate) + mMainTemplate(mainTemplate), + mMetrics(metrics), + mConfig(rootConfig) +{} + +void +Content::refresh(const Metrics& metrics, const RootConfig& config) +{ + LOG_IF(DEBUG_CONTENT).session(mSession) << "Refreshing evaluation context."; + + // We refresh imports and settings only, for now. + mMetrics = metrics; + mConfig = config; + mEvaluationContext = Context::createContentEvaluationContext( + mMetrics, + mConfig, + mMainPackage->version(), + extractTheme(mMetrics), + getSession()); + + mExtensionRequests.clear(); + mMainPackage->mDependencies.clear(); + + for (const auto& pkg : mLoaded) { + pkg.second->mDependencies.clear(); + mStashed.emplace(pkg); + } + + mLoaded.clear(); + mPending.clear(); + mRequested.clear(); + mOrderedDependencies.clear(); + + mState = LOADING; + + addImportList(*mMainPackage); + addExtensions(*mMainPackage); + + updateStatus(); +} + +void +Content::refresh(const EmbedRequest& request, const DocumentConfigPtr& documentConfig) { + auto parentHost = HostComponent::cast(request.mOriginComponent.lock()); + if (!parentHost) return; + + parentHost->refreshContent(shared_from_this(), documentConfig); +} + +void +Content::init(bool supportsEvaluation) { + if (supportsEvaluation) + mEvaluationContext = Context::createContentEvaluationContext( + mMetrics, mConfig, mMainPackage->version(), extractTheme(mMetrics), getSession()); + // First chance where we can extract settings. Set up the session. auto diagnosticLabel = getDocumentSettings()->getValue("-diagnosticLabel").asString(); mSession->setLogIdPrefix(diagnosticLabel); @@ -87,7 +168,7 @@ Content::Content(const SessionPtr& session, addExtensions(*mMainPackage); // Extract the array of main template parameters - mMainParameters = ParameterArray::parameterNames(mainTemplate); + mMainParameters = ParameterArray::parameterNames(mMainTemplate); // Extract the array of environment parameters const rapidjson::Value & json = mMainPackage->json(); @@ -130,6 +211,15 @@ Content::getRequestedPackages() return result; } +void +Content::loadPackage(const ImportRef& ref, const PackagePtr& package) +{ + LOG_IF(DEBUG_CONTENT).session(mSession) << "Adding package: " << &package; + mLoaded.emplace(ref, package); + addExtensions(*package); + addImportList(*package); +} + void Content::addPackage(const ImportRequest& request, JsonData&& raw) { @@ -178,10 +268,7 @@ Content::addPackage(const ImportRequest& request, JsonData&& raw) return; } - mLoaded.emplace(request.reference(), ptr); - addExtensions(*ptr); - // Process the import list for this package - addImportList(*ptr); + loadPackage(request.reference(), ptr); updateStatus(); } @@ -274,22 +361,99 @@ Content::addImportList(Package& package) } } -void -Content::addImport(Package& package, const rapidjson::Value& value) +bool +Content::addImport(Package& package, + const rapidjson::Value& value, + const std::string& name, + const std::string& version, + const std::set& loadAfter) { LOG_IF(DEBUG_CONTENT).session(mSession) << "addImport " << &package; + if (mState == ERROR) return false; + if (!value.IsObject()) { CONSOLE(mSession).log("Invalid import record in document"); mState = ERROR; - return; + return false; + } + + // Check for conditionality, only if context available. + if (mEvaluationContext) { + auto it_when = value.FindMember("when"); + if (it_when != value.MemberEnd()) { + auto evaluatedWhen = evaluate(*mEvaluationContext, it_when->value.GetString()); + if (!evaluatedWhen.asBoolean()) return false; + } + } + + auto typeIt = value.FindMember(PACKAGE_TYPE); + std::string type = PACKAGE_TYPE_PACKAGE; + if (typeIt != value.MemberEnd() && typeIt->value.IsString()) { + type = typeIt->value.GetString(); } - ImportRequest request(value); + if (type == PACKAGE_TYPE_ONEOF) { + auto sIt = value.FindMember(PACKAGE_ITEMS); + if (sIt != value.MemberEnd() && sIt->value.IsArray()) { + // Expansion. Can use common name/version/loadAfter. + auto commonNameAndVersion = ImportRequest::extractNameAndVersion(value, mEvaluationContext); + auto commonLoadAfter = ImportRequest::extractLoadAfter(value, mEvaluationContext); + for (const auto& s : sIt->value.GetArray()) { + if (addImport(package, s, + commonNameAndVersion.first.empty() ? name : commonNameAndVersion.first, + commonNameAndVersion.second.empty() ? version : commonNameAndVersion.second, + commonLoadAfter.empty() ? loadAfter : commonLoadAfter)) + return true; + } + } else { + CONSOLE(mSession).log("%s: Missing items field for the oneOf import", package.name().c_str()); + mState = ERROR; + return false; + } + + // If no imports were matched - use otherwise + auto otherwiseIt = value.FindMember(PACKAGE_OTHERWISE); + if (otherwiseIt != value.MemberEnd() && otherwiseIt->value.IsArray()) { + // Expansion. Can use common name/version/loadAfter. + auto commonNameAndVersion = ImportRequest::extractNameAndVersion(value, mEvaluationContext); + auto commonLoadAfter = ImportRequest::extractLoadAfter(value, mEvaluationContext); + for (const auto& s : otherwiseIt->value.GetArray()) { + if (!addImport(package, s, commonNameAndVersion.first, commonNameAndVersion.second, commonLoadAfter)) { + CONSOLE(mSession).log("%s: Otherwise imports failed", package.name().c_str()); + mState = ERROR; + return false; + } + } + } + + // Nothing was done, which is kinda fine + return true; + } else if (type == PACKAGE_TYPE_ALLOF) { + auto sIt = value.FindMember(PACKAGE_ITEMS); + if (sIt != value.MemberEnd() && sIt->value.IsArray()) { + auto commonNameAndVersion = ImportRequest::extractNameAndVersion(value, mEvaluationContext); + auto commonLoadAfter = ImportRequest::extractLoadAfter(value, mEvaluationContext); + for (const auto& s : sIt->value.GetArray()) { + addImport(package, s, + commonNameAndVersion.first.empty() ? name : commonNameAndVersion.first, + commonNameAndVersion.second.empty() ? version : commonNameAndVersion.second, + commonLoadAfter.empty() ? loadAfter : commonLoadAfter); + } + } else { + CONSOLE(mSession).log("%s: Missing items field for the allOf import", package.name().c_str()); + mState = ERROR; + return false; + } + + return true; + } + + ImportRequest request = ImportRequest::create(value, mEvaluationContext, name, version, loadAfter); if (!request.isValid()) { - CONSOLE(mSession).log("Malformed import record"); + CONSOLE(mSession).log("Malformed package import record"); mState = ERROR; - return; + return false; } package.addDependency(request.reference()); @@ -297,9 +461,19 @@ Content::addImport(Package& package, const rapidjson::Value& value) if (mRequested.find(request) == mRequested.end() && mPending.find(request) == mPending.end() && mLoaded.find(request.reference()) == mLoaded.end()) { + + // Reuse if was already resolved + auto stashed = mStashed.find(request.reference()); + if (stashed != mStashed.end()) { + loadPackage(request.reference(), stashed->second); + return true; + } + // It is a new request mRequested.insert(std::move(request)); } + + return true; } void @@ -391,7 +565,6 @@ Content::loadExtensionSettings() // get the settings for this extension from each package for (auto pkg: mOrderedDependencies) { - auto sItr = sMap.find(pkg->name()); SettingsPtr settings; @@ -413,9 +586,11 @@ Content::loadExtensionSettings() continue; // no settings for this extension // override / augment existing settings - for (auto v: val.getMap()) + for (const auto& v: val.getMap()) (*esMap)[v.first] = v.second; - LOG_IF(DEBUG_CONTENT).session(mSession) << "extension:" << name << " pkg:" << pkg << " inserting: " << val; + LOG_IF(DEBUG_CONTENT).session(mSession) << "extension:" << name + << " pkg:" << pkg + << " inserting: " << val; } } @@ -423,38 +598,62 @@ Content::loadExtensionSettings() // initialize the settings cache mExtensionSettings = std::make_shared(); // store settings Object by extension uri - for (auto tm : tmpMap) { + for (const auto& tm : tmpMap) { auto obj = (!tm.second->empty()) ? Object(tm.second) : Object::NULL_OBJECT(); mExtensionSettings->emplace(tm.first, obj); LOG_IF(DEBUG_CONTENT).session(mSession) << "extension result: " << obj.toDebugString(); } } +std::string +Content::extractTheme(const Metrics& metrics) const +{ + // If the theme is set in the document it will override the system theme + const auto& json = mMainPackage->json(); + std::string theme = metrics.getTheme(); + auto themeIter = json.FindMember("theme"); + if (themeIter != json.MemberEnd() && themeIter->value.IsString()) + theme = themeIter->value.GetString(); + return theme; +} Object -Content::getBackground(const Metrics& metrics, const RootConfig& config) const +Content::extractBackground(const Context& evaluationContext) const { const auto& json = mMainPackage->json(); auto backgroundIter = json.FindMember("background"); if (backgroundIter == json.MemberEnd()) return Color(); // Transparent - // If the theme is set in the document it will override the system theme - std::string theme = metrics.getTheme(); - auto themeIter = json.FindMember("theme"); - if (themeIter != json.MemberEnd() && themeIter->value.IsString()) - theme = themeIter->value.GetString(); + auto object = evaluate(evaluationContext, backgroundIter->value); + return asFill(evaluationContext, object); +} +Object +Content::getBackground(const Metrics& metrics, const RootConfig& config) const +{ // Create a working context and evaluate any data-binding expression // This is a restricted context because we don't load any resources or styles - auto context = Context::createBackgroundEvaluationContext( + auto context = Context::createContentEvaluationContext( metrics, config, mMainPackage->version(), - theme, + extractTheme(metrics), getSession()); - auto object = evaluate(*context, backgroundIter->value); - return asFill(*context, object); + + return extractBackground(*context); +} + +Object +Content::getBackground() const +{ + if (mEvaluationContext) { + return extractBackground(*mEvaluationContext); + } else { + LOG(LogLevel::kError).session(mSession) + << "Using extractBackground() with deprecated Content constructors. No evaluation context available."; + return Color(); + } } /** @@ -561,9 +760,13 @@ Content::getExtensionSettings(const std::string& uri) const std::map::const_iterator& es = mExtensionSettings->find(uri); if (es != mExtensionSettings->end()) { - LOG_IF(DEBUG_CONTENT).session(mSession) << "getExtensionSettings " << uri << ":" << es->second.toDebugString() - << " mapaddr:" << &es->second; - return es->second; + LOG_IF(DEBUG_CONTENT).session(mSession) << "getExtensionSettings " << uri + << ":" << es->second.toDebugString() + << " mapaddr:" << &es->second; + if (mEvaluationContext) + return evaluateNested(*mEvaluationContext, es->second); + else + return es->second; } return Object::NULL_OBJECT(); } @@ -579,6 +782,7 @@ Content::orderDependencyList() bool isOrdered = addToDependencyList(mOrderedDependencies, inProgress, mMainPackage); if (!isOrdered) CONSOLE(mSession).log("Failure to order packages"); + mStashed.clear(); return isOrdered; } @@ -591,19 +795,66 @@ Content::addToDependencyList(std::vector& ordered, std::set& inProgress, const PackagePtr& package) { - LOG_IF(DEBUG_CONTENT).session(mSession) << "addToDependencyList " << package << " dependency count=" - << package->getDependencies().size(); + LOG_IF(DEBUG_CONTENT).session(mSession) << "addToDependencyList " << package + << " dependency count=" << package->getDependencies().size(); inProgress.insert(package); // For dependency loop detection - // Start with the package dependencies - for (const auto& ref : package->getDependencies()) { + auto pds = package->getDependencies(); + auto depQueue = std::queue(); + for (const auto& pd : pds) depQueue.emplace(pd); + + std::set available; + std::set> delayed; + size_t circularCounter = 0; + while (!depQueue.empty()) { + auto ref = depQueue.front(); + depQueue.pop(); + + auto needDeps = false; + + // Check to see if package has load dependency and it was already included + for (const auto& dep : ref.loadAfter()) { + if (!ref.loadAfter().empty() && !available.count(dep)) { + // Check if we have anything else to load + if (depQueue.empty()) { + CONSOLE(mSession).log("Required loadAfter package not available %s for %s", + dep.c_str(), ref.name().c_str()); + return false; + } + + // Check if we have reverse dep + if (delayed.count(std::make_pair(dep, ref.name()))) { + CONSOLE(mSession).log("Circular package loadAfter dependency between %s and %s", + ref.name().c_str(), dep.c_str()); + return false; + } + + delayed.emplace(ref.name(), dep); + depQueue.emplace(ref); + needDeps = true; + } + } + + if (circularCounter > depQueue.size()) { + CONSOLE(mSession).log("Circular package loadAfter dependency chain"); + return false; + } + + circularCounter++; + + if (needDeps) continue; + + // Reset counter + circularCounter = 0; + LOG_IF(DEBUG_CONTENT).session(mSession) << "checking child " << ref.toString(); // Convert the reference into a loaded PackagePtr const auto& pkg = mLoaded.find(ref); if (pkg == mLoaded.end()) { - LOG(LogLevel::kError).session(mSession) << "Missing package '" << ref.name() << "' in the loaded set"; + LOG(LogLevel::kError).session(mSession) << "Missing package '" << ref.name() + << "' in the loaded set"; return false; } @@ -612,7 +863,8 @@ Content::addToDependencyList(std::vector& ordered, // Check if it is already in the dependency list (someone else included it first) auto it = std::find(ordered.begin(), ordered.end(), child); if (it != ordered.end()) { - LOG_IF(DEBUG_CONTENT).session(mSession) << "child package " << ref.toString() << " already in dependency list"; + LOG_IF(DEBUG_CONTENT).session(mSession) << "child package " << ref.toString() + << " already in dependency list"; continue; } @@ -626,9 +878,11 @@ Content::addToDependencyList(std::vector& ordered, LOG_IF(DEBUG_CONTENT).session(mSession) << "returning false with child package " << child->name(); return false; } + available.emplace(ref.name()); } - LOG_IF(DEBUG_CONTENT).session(mSession) << "Pushing package " << package << " onto ordered list"; + LOG_IF(DEBUG_CONTENT).session(mSession) << "Pushing package " << package + << " onto ordered list"; ordered.push_back(package); inProgress.erase(package); return true; diff --git a/aplcore/src/content/importrequest.cpp b/aplcore/src/content/importrequest.cpp index 610ad5d..90e70a2 100644 --- a/aplcore/src/content/importrequest.cpp +++ b/aplcore/src/content/importrequest.cpp @@ -15,29 +15,104 @@ #include "apl/content/importrequest.h" +#include "apl/engine/context.h" +#include "apl/engine/evaluate.h" + namespace apl { static const char *IMPORT_NAME = "name"; static const char *IMPORT_VERSION = "version"; static const char *IMPORT_SOURCE = "source"; +static const char *IMPORT_LOAD_AFTER = "loadAfter"; - -ImportRequest::ImportRequest(const rapidjson::Value& value) - : mValid(false), mUniqueId(ImportRequest::sNextId++) +ImportRequest +ImportRequest::create(const rapidjson::Value& value, + const ContextPtr& context, + const std::string& commonName, + const std::string& commonVersion, + const std::set& commonLoadAfter) { if (value.IsObject()) { - auto it_name = value.FindMember(IMPORT_NAME); - auto it_version = value.FindMember(IMPORT_VERSION); + auto nameAndVersion = extractNameAndVersion(value, context); + // Prefer specific name and version, use common if not provided + auto name = nameAndVersion.first; + name = name.empty() ? commonName : name; + auto version = nameAndVersion.second; + version = version.empty() ? commonVersion : version; - if (it_name != value.MemberEnd() && it_version != value.MemberEnd()) { - mReference = ImportRef(it_name->value.GetString(), it_version->value.GetString()); - mValid = true; - } + if (name.empty() || version.empty()) return {}; + // Source is always specific, if exists + std::string source; auto it_source = value.FindMember(IMPORT_SOURCE); - if (it_source != value.MemberEnd()) - mSource = it_source->value.GetString(); + if (it_source != value.MemberEnd()) { + source = it_source->value.GetString(); + if (!source.empty() && context) source = evaluate(*context, source).asString(); + } + + // Load after can also be common + auto loadAfter = extractLoadAfter(value, context); + loadAfter = loadAfter.empty() ? commonLoadAfter : loadAfter; + if (loadAfter.count(nameAndVersion.first)) return {}; + + return {name, version, source, loadAfter}; } + + return {}; +} + +ImportRequest::ImportRequest() : mValid(false), mUniqueId(ImportRequest::sNextId++) {} + +ImportRequest::ImportRequest(const std::string& name, + const std::string& version, + const std::string& source, + const std::set& loadAfter) + : mReference(name, version, source, loadAfter), mValid(true), mUniqueId(ImportRequest::sNextId++) +{ +} + +std::pair +ImportRequest::extractNameAndVersion(const rapidjson::Value& value, const ContextPtr& context) +{ + std::string name; + std::string version; + + auto it_name = value.FindMember(IMPORT_NAME); + if (it_name != value.MemberEnd()) { + name = it_name->value.GetString(); + if (context) name = evaluate(*context, name).asString(); + } + + auto it_version = value.FindMember(IMPORT_VERSION); + if (it_version != value.MemberEnd()) { + version = it_version->value.GetString(); + if (context) version = evaluate(*context, version).asString(); + } + return std::make_pair(name, version); +} + +std::set +ImportRequest::extractLoadAfter(const rapidjson::Value& value, const ContextPtr& context) +{ + std::set result; + + auto it_load_after = value.FindMember(IMPORT_LOAD_AFTER); + if (it_load_after != value.MemberEnd()) { + auto& tmpValue = it_load_after->value; + if (tmpValue.IsString()) { + auto loadString = context ? evaluate(*context, tmpValue.GetString()).asString() : tmpValue.GetString(); + result.emplace(loadString); + } else if (tmpValue.IsArray()) { + for (const auto& load : tmpValue.GetArray()) { + if (load.IsString()) { + auto depString = context ? evaluate(*context, load.GetString()).asString() : load.GetString(); + result.emplace(depString); + } + } + } + } + + return result; } uint32_t ImportRequest::sNextId = 0; diff --git a/aplcore/src/content/metrics.cpp b/aplcore/src/content/metrics.cpp index cce3064..0608160 100644 --- a/aplcore/src/content/metrics.cpp +++ b/aplcore/src/content/metrics.cpp @@ -35,8 +35,12 @@ std::string Metrics::toDebugString() const { "theme=" + mTheme + ", " "size=" + std::to_string(static_cast(mPixelWidth)) + "x" + std::to_string(static_cast(mPixelHeight)) + ", " - "autoSizeWidth=" + (mAutoSizeWidth ? "true ": "false ") + - "autoSizeHeight=" + (mAutoSizeHeight ? "true ": "false ") + + "autoSizeWidth=" + (getAutoWidth() ? "true ": "false ") + + "autoSizeHeight=" + (getAutoHeight() ? "true ": "false ") + + "minHeight=" + std::to_string(getMinHeight()) + " " + + "maxHeight=" + std::to_string(getMaxHeight()) + " " + + "minWidth=" + std::to_string(getMinWidth()) + " " + + "maxWidth=" + std::to_string(getMaxWidth()) + " " + "dpi=" + std::to_string(static_cast(mDpi)) + ", " "shape=" + sScreenShapeBimap.at(mShape) + ", " "mode=" + sViewportModeBimap.at(mMode) + ">"; diff --git a/aplcore/src/content/rootconfig.cpp b/aplcore/src/content/rootconfig.cpp index 3f6b601..acff171 100644 --- a/aplcore/src/content/rootconfig.cpp +++ b/aplcore/src/content/rootconfig.cpp @@ -326,6 +326,7 @@ RootConfig::copy() const copy->documentManager(getDocumentManager()); copy->mediaPlayerFactory(getMediaPlayerFactory()); copy->measure(getMeasure()); + copy->experimentalFeatures(getExperimentalFeatures()); for (auto key : sCopyableConfigProperties) { copy->set(key, getProperty(key)); diff --git a/aplcore/src/content/rootproperties.cpp b/aplcore/src/content/rootproperties.cpp index b70e854..3c41864 100644 --- a/aplcore/src/content/rootproperties.cpp +++ b/aplcore/src/content/rootproperties.cpp @@ -21,6 +21,8 @@ Bimap sRootPropertyBimap = { { RootProperty::kAgentName, "agentName" }, { RootProperty::kAgentVersion, "agentVersion" }, { RootProperty::kAllowOpenUrl, "allowOpenUrl" }, + { RootProperty::kDisallowDialog, "disallowDialog" }, + { RootProperty::kDisallowEditText, "disallowEditText" }, { RootProperty::kDisallowVideo, "disallowVideo" }, { RootProperty::kAnimationQuality, "animationQuality" }, { RootProperty::kDefaultIdleTimeout, "defaultIdleTimeout" }, @@ -48,8 +50,12 @@ Bimap sRootPropertyBimap = { { RootProperty::kMaxSwipeAnimationDuration, "swipeAway.maxAnimationDuration" }, { RootProperty::kMinimumFlingVelocity, "fling.minimumVelocity" }, { RootProperty::kMaximumFlingVelocity, "fling.maxVelocity" }, - { RootProperty::kTickHandlerUpdateLimit, "tickHAndlerUpdateLimit" }, + { RootProperty::kMaximumTapVelocity, "tap.maxVelocity"}, + { RootProperty::kTickHandlerUpdateLimit, "tickHandlerUpdateLimit" }, { RootProperty::kFontScale, "fontScale" }, + { RootProperty::kInitialDisplayState, "initialDisplayState"}, + { RootProperty::kLayoutDirection, "layoutDirection"}, + { RootProperty::kTextMeasurementCacheLimit, "textMeasurementCacheLimit"}, { RootProperty::kScreenMode, "screenMode" }, { RootProperty::kScreenReader, "screenReader" }, { RootProperty::kPointerInactivityTimeout, "pointerInactivityTimeout" }, diff --git a/aplcore/src/content/viewport.cpp b/aplcore/src/content/viewport.cpp index fb958ce..5161bc2 100644 --- a/aplcore/src/content/viewport.cpp +++ b/aplcore/src/content/viewport.cpp @@ -32,6 +32,10 @@ Object makeViewport( const Metrics& metrics, const std::string& theme ) { viewport->emplace("pixelHeight", metrics.getPixelHeight()); viewport->emplace("mode", metrics.getMode()); viewport->emplace("theme", theme); + viewport->emplace("minWidth", metrics.getMinWidth()); + viewport->emplace("maxWidth", metrics.getMaxWidth()); + viewport->emplace("minHeight", metrics.getMinHeight()); + viewport->emplace("maxHeight", metrics.getMaxHeight()); return viewport; } diff --git a/aplcore/src/datasource/offsetindexdatasourceconnection.cpp b/aplcore/src/datasource/offsetindexdatasourceconnection.cpp index f5e373a..9f92600 100644 --- a/aplcore/src/datasource/offsetindexdatasourceconnection.cpp +++ b/aplcore/src/datasource/offsetindexdatasourceconnection.cpp @@ -196,7 +196,7 @@ OffsetIndexDataSourceConnection::ensure(size_t index) { } } -std::shared_ptr +LiveArrayPtr OffsetIndexDataSourceConnection::getLiveArray() { return mLiveArray.lock(); } diff --git a/aplcore/src/document/coredocumentcontext.cpp b/aplcore/src/document/coredocumentcontext.cpp index e8400ee..8c77423 100644 --- a/aplcore/src/document/coredocumentcontext.cpp +++ b/aplcore/src/document/coredocumentcontext.cpp @@ -21,8 +21,9 @@ #include "apl/command/configchangecommand.h" #include "apl/command/displaystatechangecommand.h" #include "apl/command/documentcommand.h" +#include "apl/component/hostcomponent.h" +#include "apl/content/content.h" #include "apl/datasource/datasource.h" -#include "apl/datasource/datasourceprovider.h" #include "apl/engine/builder.h" #include "apl/engine/corerootcontext.h" #include "apl/engine/keyboardmanager.h" @@ -31,6 +32,7 @@ #include "apl/engine/sharedcontextdata.h" #include "apl/engine/styles.h" #include "apl/engine/uidmanager.h" +#include "apl/extension/extensionclient.h" #include "apl/extension/extensionmanager.h" #include "apl/focus/focusmanager.h" #include "apl/graphic/graphic.h" @@ -57,56 +59,6 @@ static const char *ON_MOUNT_HANDLER_NAME = "Mount"; static const std::string MOUNT_SEQUENCER = "__MOUNT_SEQUENCER"; -CoreDocumentContextPtr -CoreDocumentContext::create(const ContextPtr& context, - const ContentPtr& content, - const Object& env, - const Size& size, - const DocumentConfigPtr& documentConfig) -{ - const RootConfig& rootConfig = context->getRootConfig(); - const RootConfigPtr embeddedRootConfig = rootConfig.copy(); - embeddedRootConfig->set(RootProperty::kLang, env.opt("lang", context->getLang())); - embeddedRootConfig->set(RootProperty::kLayoutDirection, - env.opt("layoutDirection", sLayoutDirectionMap.get(context->getLayoutDirection(), ""))); - - embeddedRootConfig->set(RootProperty::kAllowOpenUrl, - rootConfig.getProperty(RootProperty::kAllowOpenUrl).getBoolean() - && env.opt("allowOpenURL", true).asBoolean()); - - auto copyDisallowProp = [&]( RootProperty propName, const std::string& envName ) { - embeddedRootConfig->set(propName, rootConfig.getProperty(propName).getBoolean() || env.opt(envName, false).asBoolean()); - }; - - copyDisallowProp(RootProperty::kDisallowDialog, "disallowDialog"); - copyDisallowProp(RootProperty::kDisallowEditText, "disallowEditText"); - copyDisallowProp(RootProperty::kDisallowVideo, "disallowVideo"); - - if (documentConfig != nullptr) { -#ifdef ALEXAEXTENSIONS - embeddedRootConfig->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider); - embeddedRootConfig->extensionMediator(documentConfig->getExtensionMediator()); -#endif - - for (const auto& provider : documentConfig->getDataSourceProviders()) { - embeddedRootConfig->dataSourceProvider(provider->getType(), provider); - } - } - - // std::lround use copied from Dimension::AbsoluteDimensionObjectType::asInt - const int width = std::lround(context->dpToPx(size.getWidth())); - const int height = std::lround(context->dpToPx(size.getHeight())); - - auto metrics = Metrics() - .size(width, height) - .shape(context->getScreenShape()) - .theme(context->getTheme().c_str()) - .dpi(context->getDpi()) - .mode(context->getViewportMode()); - - return CoreDocumentContext::create(context->getShared(), metrics, content, *embeddedRootConfig); -} - CoreDocumentContextPtr CoreDocumentContext::create( const SharedContextDataPtr& root, @@ -138,18 +90,27 @@ CoreDocumentContext::~CoreDocumentContext() { } void -CoreDocumentContext::configurationChange(const ConfigurationChange& change) +CoreDocumentContext::configurationChange(const ConfigurationChange& change, bool embedded) { // If we're in the middle of a configuration change, drop it mCore->sequencer().terminateSequencer(ConfigChangeCommand::SEQUENCER); mActiveConfigurationChanges.mergeConfigurationChange(change); - if (mActiveConfigurationChanges.empty()) + + mResultingConfigurationChange = mActiveConfigurationChanges; + // Filter/adjust change to what is suitable for embedded document. + if (embedded) { + auto parentComponent = HostComponent::cast(mContext->topComponent()->getParent()); + if (parentComponent) + mResultingConfigurationChange = parentComponent->filterConfigurationChange(mActiveConfigurationChanges, mCore->mMetrics); + } + + if (mResultingConfigurationChange.empty()) return; auto cmd = ConfigChangeCommand::create(shared_from_this(), - mActiveConfigurationChanges.asEventProperties(mCore->rootConfig(), - mCore->metrics())); + mResultingConfigurationChange.asEventProperties(mCore->rootConfig(), + mCore->metrics())); mContext->sequencer().executeOnSequencer(cmd, ConfigChangeCommand::SEQUENCER); } @@ -186,32 +147,65 @@ CoreDocumentContext::updateDisplayState(DisplayState displayState) } bool -CoreDocumentContext::reinflate(const LayoutCallbackFunc& layoutCallback) +CoreDocumentContext::refreshContent() +{ + if (mContent->isMutable()) { + auto metrics = mActiveConfigurationChanges.mergeMetrics(mCore->mMetrics); + auto config = mActiveConfigurationChanges.mergeRootConfig(mCore->mConfig); + mContent->refresh(metrics, config); + return mContent->isWaiting(); + } + + return false; +} + +const Metrics& +CoreDocumentContext::currentMetrics() const +{ + return mCore->mMetrics; +} + +const RootConfig& +CoreDocumentContext::currentConfig() const +{ + return mCore->mConfig; +} + +std::pair +CoreDocumentContext::startReinflate(std::map& preservedSequencers) { // The basic algorithm is to simply re-build CoreDocumentContexData and re-inflate the component hierarchy. // TODO: Re-use parts of the hierarchy and to maintain state during reinflation. mCore->sequencer().terminateSequencer(ConfigChangeCommand::SEQUENCER); - auto preservedSequencers = std::map(); for (auto& stp : mCore->sequencer().getSequencersToPreserve()) { preservedSequencers.emplace(stp, mCore->sequencer().detachSequencer(stp)); } auto shared = mCore->mSharedData; - if (!shared) return false; + if (!shared) return std::make_pair(false, nullptr); auto oldTop = mCore->halt(); if (oldTop) { shared->layoutManager().removeAsTopNode(oldTop); } - auto metrics = mActiveConfigurationChanges.mergeMetrics(mCore->mMetrics); - auto config = mActiveConfigurationChanges.mergeRootConfig(mCore->mConfig); + return std::make_pair(true, oldTop); +} + +bool +CoreDocumentContext::finishReinflate(const LayoutCallbackFunc& layoutCallback, const CoreComponentPtr& oldTop, const std::map& preservedSequencers) +{ + auto metrics = mResultingConfigurationChange.mergeMetrics(mCore->mMetrics); + auto config = mResultingConfigurationChange.mergeRootConfig(mCore->mConfig); // Update the configuration with the current UTC time and time adjustment config.set(RootProperty::kUTCTime, mUTCTime); config.set(RootProperty::kLocalTimeAdjustment, mLocalTimeAdjustment); + auto shared = mCore->mSharedData; + if (!shared) return false; + // The initialization routine replaces mCore with a new core init(metrics, config, shared, true); setup(oldTop); @@ -227,6 +221,7 @@ CoreDocumentContext::reinflate(const LayoutCallbackFunc& layoutCallback) // Clear the old active configuration; it is reset on a reinflation mActiveConfigurationChanges.clear(); + mResultingConfigurationChange.clear(); for (auto& ps : preservedSequencers) { if(!mCore->sequencer().reattachSequencer(ps.first, ps.second, *this)) { @@ -237,12 +232,22 @@ CoreDocumentContext::reinflate(const LayoutCallbackFunc& layoutCallback) return topComponent() != nullptr; } +bool +CoreDocumentContext::reinflate(const LayoutCallbackFunc& layoutCallback) +{ + auto preservedSequencers = std::map(); + auto startResult = startReinflate(preservedSequencers); + if (!startResult.first) return false; + + return finishReinflate(layoutCallback, startResult.second, preservedSequencers); +} + void CoreDocumentContext::resize() { // Release any "onConfigChange" action mCore->sequencer().terminateSequencer(ConfigChangeCommand::SEQUENCER); - mCore->layoutManager().configChange(mActiveConfigurationChanges, shared_from_this()); + mCore->layoutManager().configChange(mResultingConfigurationChange, shared_from_this()); // Note: we do not clear the configuration changes - there may be a reinflate() coming in the future. } @@ -278,8 +283,8 @@ CoreDocumentContext::init(const Metrics& metrics, mContext->putSystemWriteable(ELAPSED_TIME, config.getTimeManager()->currentTime()); mContext->putSystemWriteable(DISPLAY_STATE, sDisplayStateMap.at(mDisplayState)); - mUTCTime = config.getUTCTime(); - mLocalTimeAdjustment = config.getLocalTimeAdjustment(); + mUTCTime = config.getProperty(RootProperty::kUTCTime).getDouble(); + mLocalTimeAdjustment = config.getProperty(RootProperty::kLocalTimeAdjustment).getDouble(); mContext->putSystemWriteable(UTC_TIME, mUTCTime); mContext->putSystemWriteable(LOCAL_TIME, mUTCTime + mLocalTimeAdjustment); @@ -293,14 +298,16 @@ CoreDocumentContext::init(const Metrics& metrics, } } -#ifdef ALEXAEXTENSIONS // Get all known extension clients, via legacy pathway and mediator auto clients = config.getLegacyExtensionClients(); +#ifdef ALEXAEXTENSIONS auto extensionMediator = config.getExtensionMediator(); if (extensionMediator) { const auto& mediatorClients = extensionMediator->getClients(); clients.insert(mediatorClients.begin(), mediatorClients.end()); } +#endif + // Insert extension-defined live data for (auto& client : clients) { const auto& schema = client.second->extensionSchema(); @@ -309,7 +316,6 @@ CoreDocumentContext::init(const Metrics& metrics, client.second->registerObjectWatcher(ldo); } } -#endif } void @@ -549,7 +555,7 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) std::vector ordered = mContent->ordered(); // check type field of each package - auto enforceTypeField = mCore->rootConfig().getEnforceTypeField(); + auto enforceTypeField = mCore->rootConfig().getProperty(RootProperty::kEnforceTypeField).getBoolean(); if(!verifyTypeField(ordered, enforceTypeField)) { return false; } @@ -559,13 +565,12 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) return false; } - bool trackProvenance = mCore->rootConfig().getTrackProvenance(); + bool trackProvenance = mCore->rootConfig().getProperty(RootProperty::kTrackProvenance).getBoolean(); // Read settings - // Deprecated, get settings from Content->getDocumentSettings() { APL_TRACE_BEGIN("DocumentContext:readSettings"); - mCore->mSettings->read(mCore->rootConfig()); + mCore->mSettings = content()->getDocumentSettings(); APL_TRACE_END("DocumentContext:readSettings"); } @@ -696,7 +701,7 @@ CoreDocumentContext::verifyAPLVersionCompatibility(const std::vector } bool -CoreDocumentContext::verifyTypeField(const std::vector>& ordered, bool enforce) +CoreDocumentContext::verifyTypeField(const std::vector& ordered, bool enforce) { for(auto& child : ordered) { auto type = child->type(); diff --git a/aplcore/src/embed/embedrequest.cpp b/aplcore/src/embed/embedrequest.cpp index 3253e4c..945a9bf 100644 --- a/aplcore/src/embed/embedrequest.cpp +++ b/aplcore/src/embed/embedrequest.cpp @@ -17,17 +17,18 @@ #include "apl/embed/embedrequest.h" +#include "apl/component/hostcomponent.h" #include "apl/document/documentcontext.h" namespace apl { EmbedRequestPtr -EmbedRequest::create(URLRequest url, const DocumentContextPtr& origin) { - return std::make_shared(std::move(url), origin); +EmbedRequest::create(URLRequest url, const DocumentContextPtr& origin, const ComponentPtr& originComponent) { + return std::make_shared(std::move(url), origin, originComponent); } -EmbedRequest::EmbedRequest(URLRequest url, const DocumentContextPtr& origin) - : mUrl(std::move(url)), mOrigin(origin) +EmbedRequest::EmbedRequest(URLRequest url, const DocumentContextPtr& origin, const ComponentPtr& originComponent) + : mUrl(std::move(url)), mOrigin(origin), mOriginComponent(originComponent) {} const URLRequest& diff --git a/aplcore/src/engine/builder.cpp b/aplcore/src/engine/builder.cpp index 97616dd..33cf45f 100644 --- a/aplcore/src/engine/builder.cpp +++ b/aplcore/src/engine/builder.cpp @@ -258,8 +258,17 @@ Builder::expandSingleComponent(const ContextPtr& context, // Construct the component CoreComponentPtr component = CoreComponentPtr(method(expanded, std::move(properties), path)); + + bool releaseAndReturnNull = false; if (!component || !component->isValid()) { CONSOLE(context) << "Unable to inflate component"; + releaseAndReturnNull = true; + } else if (parent == nullptr && component->isDisallowed()) { + CONSOLE(context) << "Component type " << component->name() << " is top-level and disallowed, not inflating" ; + releaseAndReturnNull = true; + } + + if (releaseAndReturnNull) { if (component) component->release(); return nullptr; @@ -509,7 +518,8 @@ Builder::inflate(const ContextPtr& context, { APL_TRACE_BLOCK("Builder:inflate"); return expandLayout("mainTemplate", context, mainProperties, mainDocument, nullptr, - Path(context->getRootConfig().getTrackProvenance() ? std::string(Path::MAIN) + "/mainTemplate" : ""), true, false); + Path(context->getRootConfig().getProperty(RootProperty::kTrackProvenance).getBoolean() + ? std::string(Path::MAIN) + "/mainTemplate" : ""), true, false); } CoreComponentPtr @@ -519,10 +529,12 @@ Builder::inflate(const ContextPtr& context, assert(component.isMap() || component.isArray()); if (component.isMap()) return expandSingleComponent(context, component, Properties(), nullptr, - Path(context->getRootConfig().getTrackProvenance() ? "_virtual" : ""), true, false); + Path(context->getRootConfig().getProperty(RootProperty::kTrackProvenance).getBoolean() + ? "_virtual" : ""), true, false); else if (component.isArray()) return expandSingleComponentFromArray(context, component.getArray(), Properties(), nullptr, - Path(context->getRootConfig().getTrackProvenance() ? "_virtual" : ""), true, false); + Path(context->getRootConfig().getProperty(RootProperty::kTrackProvenance).getBoolean() + ? "_virtual" : ""), true, false); else return nullptr; } diff --git a/aplcore/src/engine/context.cpp b/aplcore/src/engine/context.cpp index 480da1c..738f9b3 100644 --- a/aplcore/src/engine/context.cpp +++ b/aplcore/src/engine/context.cpp @@ -83,7 +83,10 @@ inline ContextDataPtr createTestContextData( auto contextData = std::make_shared(nullptr, metrics, config, - RuntimeState(theme, config.getReportedAPLVersion(), false), + RuntimeState( + theme, + config.getProperty(RootProperty::kReportedVersion).getString(), + false), std::make_shared(), session, std::vector(), @@ -134,9 +137,8 @@ Context::createTypeEvaluationContext(const RootConfig& config, const std::string return contextPtr; } -// Use this to create a free-standing context. Only used for background extraction ContextPtr -Context::createBackgroundEvaluationContext( +Context::createContentEvaluationContext( const Metrics& metrics, const RootConfig& config, const std::string& aplVersion, @@ -172,31 +174,31 @@ Context::init(const Metrics& metrics, const ContextDataPtr& core) { auto env = std::make_shared(); auto& config = core->rootConfig(); - env->emplace("agentName", config.getAgentName()); - env->emplace("agentVersion", config.getAgentVersion()); - env->emplace("allowOpenURL", config.getAllowOpenUrl()); + env->emplace("agentName", config.getProperty(RootProperty::kAgentName).getString()); + env->emplace("agentVersion", config.getProperty(RootProperty::kAgentVersion).getString()); + env->emplace("allowOpenURL", config.getProperty(RootProperty::kAllowOpenUrl).getBoolean()); env->emplace("animation", config.getAnimationQualityString()); - env->emplace("aplVersion", config.getReportedAPLVersion()); + env->emplace("aplVersion", config.getProperty(RootProperty::kReportedVersion).getString()); env->emplace("disallowDialog", config.getProperty(RootProperty::kDisallowDialog).getBoolean()); env->emplace("disallowEditText", config.getProperty(RootProperty::kDisallowEditText).getBoolean()); - env->emplace("disallowVideo", config.getDisallowVideo()); + env->emplace("disallowVideo", config.getProperty(RootProperty::kDisallowVideo).getBoolean()); if (core->fullContext()) { env->emplace("extension", documentContextData(core)->extensionManager().getEnvironment()); } - env->emplace("fontScale", config.getFontScale()); + env->emplace("fontScale", config.getProperty(RootProperty::kFontScale).getDouble()); env->emplace("lang", core->getLang()); env->emplace("layoutDirection", sLayoutDirectionMap.get(core->getLayoutDirection(), "")); env->emplace("screenMode", config.getScreenMode()); - env->emplace("screenReader", config.getScreenReaderEnabled()); + env->emplace("screenReader", config.getProperty(RootProperty::kScreenReader).getBoolean()); env->emplace("reason", core->getReinflationFlag() ? "reinflation" : "initial"); env->emplace("documentAPLVersion", core->getRequestedAPLVersion()); auto timing = std::make_shared(); - timing->emplace("doublePressTimeout", config.getDoublePressTimeout()); - timing->emplace("longPressTimeout", config.getLongPressTimeout()); - timing->emplace("minimumFlingVelocity", config.getMinimumFlingVelocity()); - timing->emplace("pressedDuration", config.getPressedDuration()); - timing->emplace("tapOrScrollTimeout", config.getTapOrScrollTimeout()); + timing->emplace("doublePressTimeout", config.getProperty(RootProperty::kDoublePressTimeout).getDouble()); + timing->emplace("longPressTimeout", config.getProperty(RootProperty::kLongPressTimeout).getDouble()); + timing->emplace("minimumFlingVelocity", config.getProperty(RootProperty::kMinimumFlingVelocity).getDouble()); + timing->emplace("pressedDuration", config.getProperty(RootProperty::kPressedDuration).getDouble()); + timing->emplace("tapOrScrollTimeout", config.getProperty(RootProperty::kTapOrScrollTimeout).getDouble()); timing->emplace("maximumTapVelocity", config.getProperty(RootProperty::kMaximumTapVelocity).getInteger()); env->emplace("timing", timing); diff --git a/aplcore/src/engine/corerootcontext.cpp b/aplcore/src/engine/corerootcontext.cpp index c887960..54efddc 100644 --- a/aplcore/src/engine/corerootcontext.cpp +++ b/aplcore/src/engine/corerootcontext.cpp @@ -19,6 +19,7 @@ #include "apl/action/scrolltoaction.h" #include "apl/command/arraycommand.h" +#include "apl/content/content.h" #include "apl/datasource/datasource.h" #include "apl/datasource/datasourceprovider.h" #include "apl/embed/documentregistrar.h" @@ -37,6 +38,7 @@ #include "apl/time/sequencer.h" #include "apl/time/timemanager.h" #include "apl/touch/pointermanager.h" +#include "apl/utils/make_unique.h" #include "apl/utils/tracing.h" #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" @@ -50,9 +52,9 @@ static const std::string SCROLL_TO_RECT_SEQUENCER = "__SCROLL_TO_RECT_SEQUENCE"; RootContextPtr CoreRootContext::create(const Metrics& metrics, - const ContentPtr& content, - const RootConfig& config, - std::function callback) + const ContentPtr& content, + const RootConfig& config, + std::function callback) { if (!content->isReady()) { LOG(LogLevel::kError).session(content) << "Attempting to create root context with illegal content"; @@ -75,8 +77,8 @@ CoreRootContext::create(const Metrics& metrics, } CoreRootContext::CoreRootContext(const Metrics& metrics, - const ContentPtr& content, - const RootConfig& config) + const ContentPtr& content, + const RootConfig& config) : mTimeManager(config.getTimeManager()), mDisplayState(static_cast(config.getProperty(RootProperty::kInitialDisplayState).getInteger())) { @@ -85,20 +87,25 @@ CoreRootContext::CoreRootContext(const Metrics& metrics, CoreRootContext::~CoreRootContext() { assert(mShared); mTimeManager->terminate(); - mTopDocument = nullptr; mShared->halt(); + mTopDocument = nullptr; } void CoreRootContext::configurationChange(const ConfigurationChange& change) { assert(mTopDocument); - mTopDocument->configurationChange(change); - - mShared->documentRegistrar().forEach([change](const std::shared_ptr& document) { - // Pass change through as is, document will figure it out itself - return document->configurationChange(change); - }); + mTopDocument->configurationChange(change, false); + + if (!mShared->documentRegistrar().list().empty()) { + if (!change.empty()) { + mShared->documentRegistrar().forEach( + [change](const CoreDocumentContextPtr& document) { + // Pass change through as is, document will figure it out itself + return document->configurationChange(change, true); + }); + } + } } void @@ -131,11 +138,14 @@ CoreRootContext::init(const Metrics& metrics, APL_TRACE_BLOCK("RootContext:init"); mShared = std::make_shared(shared_from_this(), metrics, config); + // Initialize the viewport size to the default size + mViewportSize = {metrics.getWidth(), metrics.getHeight()}; + mTopDocument = CoreDocumentContext::create(mShared, metrics, content, config); // Hm. Time is interesting. Because it's actually initialized in the context. - mUTCTime = config.getUTCTime(); - mLocalTimeAdjustment = config.getLocalTimeAdjustment(); + mUTCTime = config.getProperty(RootProperty::kUTCTime).getDouble(); + mLocalTimeAdjustment = config.getProperty(RootProperty::kLocalTimeAdjustment).getDouble(); } void @@ -152,7 +162,7 @@ CoreRootContext::clearPendingInternal(bool first) const APL_TRACE_BLOCK("RootContext:clearPending"); // Flush any dynamic data changes, for all documents mTopDocument->mCore->dataManager().flushDirty(); - mShared->documentRegistrar().forEach([](const std::shared_ptr& document) { + mShared->documentRegistrar().forEach([](const CoreDocumentContextPtr& document) { return document->mCore->dataManager().flushDirty(); }); @@ -167,9 +177,13 @@ CoreRootContext::clearPendingInternal(bool first) const // Clear pending on all docs mTopDocument->clearPending(); - mShared->documentRegistrar().forEach([](const std::shared_ptr& document) { + mShared->documentRegistrar().forEach([](const CoreDocumentContextPtr& document) { return document->clearPending(); }); + + for (const auto& comp : mShared->dirtyComponents().getAll()) { + CoreComponent::cast(comp)->postClearPending(); + } } bool @@ -385,7 +399,7 @@ CoreRootContext::updateTimeInternal(apl_time_t elapsedTime, apl_time_t utcTime) APL_TRACE_BEGIN("RootContext:systemUpdateAndRecalculateTime"); mTopDocument->updateTime(mUTCTime, mLocalTimeAdjustment); - mShared->documentRegistrar().forEach([&](const std::shared_ptr& document) { + mShared->documentRegistrar().forEach([&](const CoreDocumentContextPtr& document) { document->updateTime(mUTCTime, mLocalTimeAdjustment); }); APL_TRACE_END("RootContext:systemUpdateAndRecalculateTime"); @@ -463,12 +477,7 @@ CoreRootContext::setup(bool reinflate) } else { // Update LayoutManager with the new overall size const auto& metrics = mTopDocument->mCore->metrics(); - mShared->layoutManager().setSize( - Size( - static_cast(metrics.getWidth()), - static_cast(metrics.getHeight()) - ) - ); + mShared->layoutManager().setSize(metrics.getViewportSize()); } mShared->layoutManager().firstLayout(); @@ -668,15 +677,16 @@ CoreRootContext::content() const return mTopDocument->content(); } -bool -CoreRootContext::getAutoWidth() const +Size +CoreRootContext::getViewportSize() const { - return mTopDocument->mCore->metrics().getAutoWidth(); } + return mViewportSize; +} -bool -CoreRootContext::getAutoHeight() const +void +CoreRootContext::setViewportSize(float width, float height) const { - return mTopDocument->mCore->metrics().getAutoHeight(); + mViewportSize = { width, height }; } #ifdef SCENEGRAPH @@ -698,6 +708,8 @@ CoreRootContext::getSceneGraph() if (!mSceneGraph) mSceneGraph = sg::SceneGraph::create(); + mSceneGraph->setViewportSize(mViewportSize); + if (mSceneGraph->getLayer()) { mSceneGraph->updates().clear(); for (auto& component : getDirty()) diff --git a/aplcore/src/engine/layoutmanager.cpp b/aplcore/src/engine/layoutmanager.cpp index 6fada42..3981a64 100644 --- a/aplcore/src/engine/layoutmanager.cpp +++ b/aplcore/src/engine/layoutmanager.cpp @@ -16,11 +16,11 @@ #include "apl/engine/layoutmanager.h" #include "apl/component/corecomponent.h" +#include "apl/component/yogaproperties.h" #include "apl/content/configurationchange.h" #include "apl/document/coredocumentcontext.h" #include "apl/engine/corerootcontext.h" #include "apl/livedata/layoutrebuilder.h" -#include "apl/primitives/size.h" #include "apl/utils/tracing.h" namespace apl { @@ -36,7 +36,7 @@ yogaNodeDirtiedCallback(YGNodeRef node) component->getContext()->layoutManager().requestLayout(component->shared_from_corecomponent(), false); } -LayoutManager::LayoutManager(const CoreRootContext& coreRootContext, const Size& size) +LayoutManager::LayoutManager(const CoreRootContext& coreRootContext, ViewportSize size) : mRoot(coreRootContext), mConfiguredSize(size) { @@ -50,7 +50,7 @@ LayoutManager::terminate() } void -LayoutManager::setSize(const Size& size) +LayoutManager::setSize(ViewportSize size) { mConfiguredSize = size; } @@ -87,20 +87,17 @@ LayoutManager::configChange(const ConfigurationChange& change, if (mTerminated) return; - auto top = CoreComponent::cast(document->topComponent()); - Size size = top->getLayoutSize(); + if (!change.hasSizeChange()) + return; - // Update the global size to match the configuration change - if (change.hasSizeChange()) { - size = change.getSize() * mRoot.getPxToDp(); - if (!document->isEmbedded()) { - mConfiguredSize = size; - } - } + // Update the global size to match the configuration change only if we are not embedded + auto size = change.getSize(mRoot.getPxToDp()); + if (!document->isEmbedded()) + mConfiguredSize = size; - // If there is a size mismatch, schedule a layout - if (top && top->getLayoutSize() != size) - requestLayout(top, false); + auto top = CoreComponent::cast(document->topComponent()); + if (top && (!size.isFixed() || top->getLayoutSize() != size.nominalSize())) + requestLayout(top, true); } static bool @@ -201,46 +198,141 @@ LayoutManager::removeAsTopNode(const CoreComponentPtr& component) } bool -LayoutManager::isTopNode(const std::shared_ptr& component) const +LayoutManager::isTopNode(const ConstCoreComponentPtr& component) const { assert(component); return YGNodeGetDirtiedFunc(component->getNode()); } +std::pair +LayoutManager::getMinMaxWidth(const CoreComponent& component) const +{ + auto minWidth = 1.0f; + auto maxWidth = mConfiguredSize.maxWidth; + + if (component.getCalculated(kPropertyMinWidth).asNumber() != 0) + minWidth = YGNodeStyleGetMinWidth(component.getNode()).value; + + if (!component.getCalculated(kPropertyMaxWidth).isNull()) + maxWidth = std::min(maxWidth, YGNodeStyleGetMaxWidth(component.getNode()).value); + + return std::make_pair(minWidth, maxWidth); +} + +std::pair +LayoutManager::getMinMaxHeight(const CoreComponent& component) const +{ + auto minHeight = 1.0f; + auto maxHeight = mConfiguredSize.maxHeight; + + if (component.getCalculated(kPropertyMinHeight).asNumber() != 0) + minHeight = YGNodeStyleGetMinHeight(component.getNode()).value; + + if (!component.getCalculated(kPropertyMaxHeight).isNull()) + maxHeight = std::min(maxHeight, YGNodeStyleGetMaxHeight(component.getNode()).value); + + return std::make_pair(minHeight, maxHeight); +} + void LayoutManager::layoutComponent(const CoreComponentPtr& component, bool useDirtyFlag, bool first) { APL_TRACE_BLOCK("LayoutManager:layoutComponent"); - auto parent = component->getParent(); + auto parent = CoreComponent::cast(component->getParent()); LOG_IF(DEBUG_LAYOUT_MANAGER) << "component=" << component->toDebugSimpleString() << " dirty_flag=" << useDirtyFlag << " parent=" << (parent ? parent->toDebugSimpleString() : "none"); - Size size; - float width, height; + ViewportSize viewportSize = {}; + float overallWidth, overallHeight; + auto node = component->getNode(); if (!parent) { // Top component - size = mConfiguredSize; - width = mRoot.getAutoWidth() ? YGUndefined : size.getWidth(); - height = mRoot.getAutoHeight() ? YGUndefined : size.getHeight(); + viewportSize = mConfiguredSize; + size = viewportSize.layoutSize(); // This will have -1 for variable width/height sizes + overallWidth = viewportSize.width; + overallHeight = viewportSize.height; + + if (viewportSize.isAutoWidth() && YGNodeStyleGetWidth(node) == YGValueAuto) + overallWidth = YGUndefined; + + if (viewportSize.isAutoHeight() && YGNodeStyleGetHeight(node) == YGValueAuto) + overallHeight = YGUndefined; } else { size = parent->getCalculated(kPropertyInnerBounds).get().getSize(); - if (size == Size()) - return; - width = size.getWidth(); - height = size.getHeight(); - } - auto node = component->getNode(); + auto autoWidth = parent->getCalculated(kPropertyWidth).isAutoDimension(); + auto autoHeight = parent->getCalculated(kPropertyHeight).isAutoDimension(); + + // This check is irrelevant for autosizing + if (size == Size() && !(autoWidth || autoHeight)) return; + + overallWidth = size.getWidth(); + overallHeight = size.getHeight(); + + if (autoWidth) { + overallWidth = YGUndefined; + auto minmax = getMinMaxWidth(*parent); + viewportSize.minWidth = minmax.first; + viewportSize.maxWidth = minmax.second; + } + if (autoHeight) { + overallHeight = YGUndefined; + auto minmax = getMinMaxHeight(*parent); + viewportSize.minHeight = minmax.first; + viewportSize.maxHeight = minmax.second; + } + + size = Size(autoWidth ? -1 : size.getWidth(), + autoHeight ? -1 : size.getHeight()); + } // Layout the component if it has a dirty Yoga node OR if the cached size doesn't match the target size - // Note that the top-level component may get laid out multiple times if it auto sizes. + // The top-level component may get laid out multiple times if it auto sizes. if (YGNodeIsDirty(node) || size != component->getLayoutSize()) { component->preLayoutProcessing(useDirtyFlag); APL_TRACE_BEGIN("LayoutManager:YGNodeCalculateLayout"); - YGNodeCalculateLayout(node, width, height, component->getLayoutDirection()); + + YGNodeCalculateLayout(node, overallWidth, overallHeight, component->getLayoutDirection()); + + // If we were allowing the overall width to vary, then the node width was "auto". + // Re-layout the node with a fixed width that is clipped to min/max + if (YGFloatIsUndefined(overallWidth)) { + overallWidth = YGNodeLayoutGetWidth(node); + if (overallWidth > viewportSize.maxWidth) + overallWidth = viewportSize.maxWidth; + if (overallWidth < viewportSize.minWidth) + overallWidth = viewportSize.minWidth; + YGNodeCalculateLayout(node, overallWidth, overallHeight, component->getLayoutDirection()); + } + else if (viewportSize.isAutoWidth() && YGNodeStyleGetWidth(node).unit == YGUnit::YGUnitPoint) { + overallWidth = YGNodeLayoutGetWidth(node); + if (overallWidth > viewportSize.maxWidth) + overallWidth = viewportSize.maxWidth; + if (overallWidth < viewportSize.minWidth) + overallWidth = viewportSize.minWidth; + } + + // If we were allowing the overall height to vary, then the node height was "auto". + // Re-layout the node with a fixed height that is clipped to min/max + if (YGFloatIsUndefined(overallHeight)) { + overallHeight = YGNodeLayoutGetHeight(node); + if (overallHeight > viewportSize.maxHeight) + overallHeight = viewportSize.maxHeight; + if (overallHeight < viewportSize.minHeight) + overallHeight = viewportSize.minHeight; + YGNodeCalculateLayout(node, overallWidth, overallHeight, component->getLayoutDirection()); + } + else if (viewportSize.isAutoHeight() && YGNodeStyleGetHeight(node).unit == YGUnit::YGUnitPoint) { + overallHeight = YGNodeLayoutGetHeight(node); + if (overallHeight > viewportSize.maxHeight) + overallHeight = viewportSize.maxHeight; + if (overallHeight < viewportSize.minHeight) + overallHeight = viewportSize.minHeight; + } + APL_TRACE_END("LayoutManager:YGNodeCalculateLayout"); component->processLayoutChanges(useDirtyFlag, first); @@ -251,7 +343,22 @@ LayoutManager::layoutComponent(const CoreComponentPtr& component, bool useDirtyF } } - // Cache the laid-out size of the component. + if (!parent) + mRoot.setViewportSize(overallWidth, overallHeight); + else { + // Set parent size to something reasonable (and not auto) and request parent's + // re-layout, if auto + if (parent->getCalculated(kPropertyWidth).isAutoDimension()) { + yn::setWidth(parent->getNode(), overallWidth, *parent->getContext()); + requestLayout(CoreComponent::cast(parent->getContext()->topComponent()), false); + } + if (parent->getCalculated(kPropertyHeight).isAutoDimension()) { + yn::setHeight(parent->getNode(), overallHeight, *parent->getContext()); + requestLayout(CoreComponent::cast(parent->getContext()->topComponent()), false); + } + } + + // Cache the laid-out size of the component. -1 values are for variable viewport sizes component->setLayoutSize(size); } diff --git a/aplcore/src/engine/sharedcontextdata.cpp b/aplcore/src/engine/sharedcontextdata.cpp index 2f9d42c..3f57189 100644 --- a/aplcore/src/engine/sharedcontextdata.cpp +++ b/aplcore/src/engine/sharedcontextdata.cpp @@ -67,17 +67,16 @@ ygLogger(const YGConfigRef config, return 1; // Does this matter? } -SharedContextData::SharedContextData(const CoreRootContextPtr& root, const Metrics& metrics, +SharedContextData::SharedContextData(const CoreRootContextPtr& root, + const Metrics& metrics, const RootConfig& config) - : mRequestedVersion(config.getReportedAPLVersion()), + : mRequestedVersion(config.getProperty(RootProperty::kReportedVersion).getString()), mDocumentRegistrar(std::make_unique()), mFocusManager(std::make_unique(*root)), mHoverManager(std::make_unique(*root)), mPointerManager(std::make_unique(*root, *mHoverManager)), mKeyboardManager(std::make_unique()), - mLayoutManager(std::make_unique( - *root, - Size(static_cast(metrics.getWidth()), static_cast(metrics.getHeight())))), + mLayoutManager(std::make_unique(*root, metrics.getViewportSize())), mTickScheduler(std::make_unique(config.getTimeManager())), mDirtyComponents(std::make_unique()), mUniqueIdGenerator(std::make_unique()), @@ -102,7 +101,7 @@ SharedContextData::SharedContextData(const CoreRootContextPtr& root, const Metri } SharedContextData::SharedContextData(const RootConfig& config) - : mRequestedVersion(config.getReportedAPLVersion()), + : mRequestedVersion(config.getProperty(RootProperty::kReportedVersion).getString()), mUniqueIdGenerator(std::make_unique()), mDependantManager(std::make_unique()), mYGConfigRef(YGConfigNew()), @@ -122,6 +121,7 @@ SharedContextData::~SharedContextData() { void SharedContextData::halt() { + mFocusManager->terminate(); mLayoutManager->terminate(); mTimeManager->clear(); mEventManager->clear(); diff --git a/aplcore/src/engine/tickscheduler.cpp b/aplcore/src/engine/tickscheduler.cpp index aa15347..4bbc98e 100644 --- a/aplcore/src/engine/tickscheduler.cpp +++ b/aplcore/src/engine/tickscheduler.cpp @@ -44,7 +44,7 @@ TickScheduler::processTickHandlers(const CoreDocumentContextPtr& documentContext for (const auto& handler : tickHandlers.getArray()) { auto delay = std::max(propertyAsDouble(documentContext->context(), handler, "minimumDelay", 1000), - documentContext->rootConfig().getTickHandlerUpdateLimit()); + documentContext->rootConfig().getProperty(RootProperty::kTickHandlerUpdateLimit).getDouble()); scheduleTickHandler(std::weak_ptr(documentContext), handler, delay); } } diff --git a/aplcore/src/extension/extensionclient.cpp b/aplcore/src/extension/extensionclient.cpp index bab47fb..a2bbc21 100644 --- a/aplcore/src/extension/extensionclient.cpp +++ b/aplcore/src/extension/extensionclient.cpp @@ -782,7 +782,7 @@ ExtensionClient::readExtensionTypes(const Context& context, const Object& types) name.getString().c_str(), mUri.c_str()); continue; } else { - defValue = propertyAsObject(context, ps, "default"); + defValue = propertyAsRecursive(context, ps, "default"); ptype = propertyAsMapped(context, ps, "type", kBindingTypeAny, sBindingMap); if (!sBindingMap.has(ptype)) { ptype = kBindingTypeAny; diff --git a/aplcore/src/extension/extensioncomponent.cpp b/aplcore/src/extension/extensioncomponent.cpp index 0dac523..f5c3196 100644 --- a/aplcore/src/extension/extensioncomponent.cpp +++ b/aplcore/src/extension/extensioncomponent.cpp @@ -37,8 +37,8 @@ ExtensionComponent::create(const ExtensionComponentDefinition& definition, return component; } -std::shared_ptr -ExtensionComponent::cast(const std::shared_ptr& component) { +ExtensionComponentPtr +ExtensionComponent::cast(const ComponentPtr& component) { return component && component->getType() == ComponentType::kComponentTypeExtension ? std::static_pointer_cast(component) : nullptr; } diff --git a/aplcore/src/extension/extensionmediator.cpp b/aplcore/src/extension/extensionmediator.cpp index 372be00..e0c4199 100644 --- a/aplcore/src/extension/extensionmediator.cpp +++ b/aplcore/src/extension/extensionmediator.cpp @@ -17,7 +17,9 @@ #include "apl/extension/extensionmediator.h" +#include #include +#include #include #include #include @@ -25,6 +27,7 @@ #include #include +#include "apl/content/content.h" #include "apl/content/extensionrequest.h" #include "apl/document/coredocumentcontext.h" #include "apl/extension/extensioncomponent.h" @@ -396,8 +399,13 @@ ExtensionMediator::grantExtension(const Object& flags, const std::string& uri) return; } - if (extensionProvider->hasExtension(uri)) { - auto required = mRequired.count(uri); + auto required = mRequired.count(uri); + auto extensionExists = extensionProvider->hasExtension(uri); + if (required && !extensionExists) { + mFailState = true; + CONSOLE(mSession) << "Provider doesn't have required extension: " << uri; + return; + } else if (extensionExists) { // First get will call initialize. auto proxy = extensionProvider->getExtension(uri); if (!proxy) { @@ -599,7 +607,7 @@ ExtensionMediator::loadExtensions( ExtensionsLoadedCallback loaded) { auto callbackV2 = [loaded](bool) { loaded(); }; - loadExtensions(rootConfig, content, std::move(callbackV2)); + loadExtensions(rootConfig->getExtensionFlags(), content, std::move(callbackV2)); } void @@ -1040,6 +1048,20 @@ ExtensionMediator::unregister(const alexaext::ActivityDescriptorPtr& activity) { } } +std::unordered_map +ExtensionMediator::getLoadedExtensions() +{ + std::unordered_map loaded; + std::copy_if(mActivitiesByURI.begin(), mActivitiesByURI.end(), std::inserter(loaded, loaded.end()), [this](decltype(loaded)::value_type const& kv_pair) { + auto client = mClients.find(kv_pair.first); + if (client != mClients.end()) { + return client->second->registered(); + } + return false; + }); + return loaded; +} + } // namespace apl diff --git a/aplcore/src/focus/focusmanager.cpp b/aplcore/src/focus/focusmanager.cpp index 08cfbf1..f66a51a 100644 --- a/aplcore/src/focus/focusmanager.cpp +++ b/aplcore/src/focus/focusmanager.cpp @@ -67,8 +67,11 @@ scrollIntoView(const std::shared_ptr& timers, const CoreComponentPt } void -FocusManager::setFocus(const CoreComponentPtr& component, bool notifyViewhost) +FocusManager::setFocus(const CoreComponentPtr& component, bool notifyViewhost, bool shouldScrollIntoView) { + if (mTerminated) + return; + // Specifying a null component will clear existing focus, if applicable if (!component) { clearFocus(notifyViewhost); @@ -101,22 +104,30 @@ FocusManager::setFocus(const CoreComponentPtr& component, bool notifyViewhost) if (notifyViewhost) { auto timers = mCore.rootConfig().getTimeManager(); mCore.sequencer().terminateSequencer(FOCUS_RELEASE_SEQUENCER); - // Get target into viewport (a.g. scroll it in) - auto action = scrollIntoView(timers, component); - if (action && action->isPending()) { - auto wrapped = Action::wrapWithCallback(timers, action, [this](bool, const ActionPtr&) { - reportFocusedComponent(); - }); - mCore.sequencer().attachToSequencer(wrapped, FOCUS_SEQUENCER); - } else { - reportFocusedComponent(); + if (shouldScrollIntoView) { + // Get target into viewport (a.g. scroll it in) + auto action = scrollIntoView(timers, component); + if (action && action->isPending()) { + auto wrapped = Action::wrapWithCallback(timers, action, [this](bool, const ActionPtr&) { + // If terminated, we want to skip reporting a focused component because the data we need no longer exists. + if (!mTerminated) { + reportFocusedComponent(); + } + }); + mCore.sequencer().attachToSequencer(wrapped, FOCUS_SEQUENCER); + return; + } } + reportFocusedComponent(); } } void FocusManager::releaseFocus(const std::shared_ptr& component, bool notifyViewhost) { + if (mTerminated) + return; + auto focused = mFocused.lock(); LOG_IF(DEBUG_FOCUS).session(component) << focused << " -> " << component; @@ -131,6 +142,9 @@ FocusManager::releaseFocus(const std::shared_ptr& component, void FocusManager::clearFocus(bool notifyViewhost, FocusDirection direction, bool force) { + if (mTerminated) + return; + auto focused = mFocused.lock(); if (focused) { @@ -202,6 +216,9 @@ inline Rect generateOrigin(FocusDirection direction, const Rect& viewport) bool FocusManager::focus(FocusDirection direction) { + if (mTerminated) + return false; + if (mFinder) { auto focused = mFocused.lock(); if (focused) { @@ -226,6 +243,9 @@ FocusManager::focus(FocusDirection direction) bool FocusManager::focus(FocusDirection direction, const Rect& origin) { + if (mTerminated) + return false; + if (mFinder) { auto next = find(direction, origin); if (next) { @@ -243,6 +263,9 @@ FocusManager::focus(FocusDirection direction, const Rect& origin) bool FocusManager::focus(FocusDirection direction, const Rect& origin, const CoreComponentPtr& root) { + if (mTerminated) + return false; + if (mFinder) { auto next = mFinder->findNext(mFocused.lock(), origin, direction, root); if (next) { @@ -259,6 +282,9 @@ FocusManager::focus(FocusDirection direction, const Rect& origin, const CoreComp CoreComponentPtr FocusManager::find(FocusDirection direction) { + if (mTerminated) + return nullptr; + auto focused = mFocused.lock(); if (focused) { return mFinder->findNext(focused, direction); @@ -270,19 +296,29 @@ FocusManager::find(FocusDirection direction) CoreComponentPtr FocusManager::find(FocusDirection direction, const Rect& origin) { + if (mTerminated) + return nullptr; + return mFinder->findNext(mFocused.lock(), origin, direction, CoreComponent::cast(mCore.topComponent())); } CoreComponentPtr FocusManager::find(FocusDirection direction, const CoreComponentPtr& origin, const Rect& originRect, const CoreComponentPtr& root) { + if (mTerminated) + return nullptr; + return mFinder->findNext(origin, originRect, direction, root); } std::map FocusManager::getFocusableAreas() -{ +{ std::map result; + + if (mTerminated) + return result; + auto root = CoreComponent::cast(mCore.topComponent()); auto focusables = mFinder->getFocusables(root, false); if(root->isFocusable()) { @@ -298,4 +334,11 @@ FocusManager::getFocusableAreas() return result; } +void FocusManager::terminate() +{ + mTerminated = true; + mCore.sequencer().terminateSequencer(FOCUS_RELEASE_SEQUENCER); + mCore.sequencer().terminateSequencer(FOCUS_SEQUENCER); +} + } diff --git a/aplcore/src/graphic/graphic.cpp b/aplcore/src/graphic/graphic.cpp index 340163c..552a8fa 100644 --- a/aplcore/src/graphic/graphic.cpp +++ b/aplcore/src/graphic/graphic.cpp @@ -57,7 +57,7 @@ GraphicPtr Graphic::create(const ContextPtr& context, const JsonResource& jsonResource, Properties&& properties, - const std::shared_ptr& component) + const CoreComponentPtr& component) { return create(context, jsonResource.json(), @@ -71,7 +71,7 @@ GraphicPtr Graphic::create(const ContextPtr& context, const rapidjson::Value& json, Properties&& properties, - const std::shared_ptr& component, + const CoreComponentPtr& component, const Path& path, const StyleInstancePtr& styledPtr) { @@ -164,7 +164,7 @@ void Graphic::initialize(const ContextPtr& sourceContext, const rapidjson::Value& json, Properties&& properties, - const std::shared_ptr& component, + const CoreComponentPtr& component, const Path& path, const StyleInstancePtr& styledPtr) { diff --git a/aplcore/src/graphic/graphicelementtext.cpp b/aplcore/src/graphic/graphicelementtext.cpp index dbf69f8..1ffe6ed 100644 --- a/aplcore/src/graphic/graphicelementtext.cpp +++ b/aplcore/src/graphic/graphicelementtext.cpp @@ -52,7 +52,7 @@ GraphicElementText::create(const GraphicPtr &graphic, inline Object defaultFontFamily(GraphicElement&, const RootConfig& rootConfig) { - return Object(rootConfig.getDefaultFontFamily()); + return rootConfig.getProperty(RootProperty::kDefaultFontFamily); } const GraphicPropDefSet& diff --git a/aplcore/src/media/mediatrack.cpp b/aplcore/src/media/mediatrack.cpp index a2de890..1caee5b 100644 --- a/aplcore/src/media/mediatrack.cpp +++ b/aplcore/src/media/mediatrack.cpp @@ -21,4 +21,57 @@ Bimap sTextTrackTypeMap = { {kTextTrackTypeCaption, "caption"}, }; +// Utility function that creates a MediaTrack from a Speech Object +MediaTrack createMediaTrack(const Object &speech, const std::shared_ptr &context) { + const std::string SPEECH_URL = "url"; + const std::string SPEECH_TEXT_TRACK = "textTrack"; + const std::string TEXT_TRACK_CONTENT = "content"; + const std::string TEXT_TRACK_TYPE = "type"; + const std::string TEXT_TRACK_TYPE_CAPTION = "caption"; + + std::string url; + TextTrackArray trackArray; + // if speech is a string then it is a single url + if (speech.isString()) { + url = speech.getString(); + if (url.empty()) { + CONSOLE(context).log("Audio source missing in playback"); + } + } + + // if speech is a map then it may contain a textTrack + else if (speech.isMap()) { + url = speech.get(SPEECH_URL).asString(); + if (!url.empty()) { + auto textTrack = speech.get(SPEECH_TEXT_TRACK); + auto content = textTrack.get(TEXT_TRACK_CONTENT).asString(); + auto type = textTrack.get(TEXT_TRACK_TYPE).asString(); + if (!content.empty()) { + if (!type.empty()) { + if (type == TEXT_TRACK_TYPE_CAPTION) { + // A textTrack is only valid if it has a source and a type of caption + trackArray.push_back(TextTrack{kTextTrackTypeCaption, content, ""}); + } else { + CONSOLE(context).log("TextTrack has an invalid type"); + } + } else { + CONSOLE(context).log("TextTrack is missing a type"); + } + + } else { + CONSOLE(context).log("TextTrack is missing an url"); + } + } + } + + return MediaTrack{ + url, // URL + 0, // Start + 0, // Duration (play the entire track) + 0, // Repeat Count + {}, // Headers + trackArray // textTrack Array + }; +} + } // namespace apl \ No newline at end of file diff --git a/aplcore/src/primitives/accessibilityaction.cpp b/aplcore/src/primitives/accessibilityaction.cpp index 43ec5b5..0f1c737 100644 --- a/aplcore/src/primitives/accessibilityaction.cpp +++ b/aplcore/src/primitives/accessibilityaction.cpp @@ -17,14 +17,20 @@ #include "apl/component/corecomponent.h" #include "apl/engine/typeddependant.h" #include "apl/engine/dependantmanager.h" -#include "apl/engine/evaluate.h" #include "apl/engine/propdef.h" -#include "apl/primitives/boundsymbolset.h" #include "apl/time/sequencer.h" #include "apl/utils/session.h" namespace apl { +const char* AccessibilityAction::ACCESSIBILITY_ACTION_ACTIVATE = "activate"; +const char* AccessibilityAction::ACCESSIBILITY_ACTION_TAP = "tap"; +const char* AccessibilityAction::ACCESSIBILITY_ACTION_DOUBLETAP = "doubletap"; +const char* AccessibilityAction::ACCESSIBILITY_ACTION_LONGPRESS = "longpress"; +const char* AccessibilityAction::ACCESSIBILITY_ACTION_SWIPEAWAY = "swipeaway"; +const char* AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD = "scrollforward"; +const char* AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD = "scrollbackward"; + /** * The accessibility action can have a dynamic "enabled" property. This dependant is used * to track when the equation driving "enabled" changes. If we need to make other accessibility @@ -34,7 +40,7 @@ namespace apl { using AccessibilityActionDependent = TypedDependant; -std::shared_ptr +AccessibilityActionPtr AccessibilityAction::create(const CoreComponentPtr& component, const Object& object) { auto context = component->getContext(); @@ -66,6 +72,13 @@ AccessibilityAction::create(const CoreComponentPtr& component, const Object& obj return aa; } +AccessibilityActionPtr +AccessibilityAction::create( + const CoreComponentPtr& component, const std::string& name, const std::string& label) +{ + return std::make_shared(component, name, label); +} + void AccessibilityAction::initialize(const ContextPtr& context, const Object& object) { @@ -116,9 +129,11 @@ AccessibilityAction::setValue(AccessibilityActionKey key, const Object& value, b if (enabled != mEnabled) { mEnabled = enabled; if (useDirtyFlag) { - auto lock = mComponent.lock(); - if (lock) + auto lock = CoreComponent::cast(mComponent.lock()); + if (lock) { + // Preserves old behavior lock->setDirty(kPropertyAccessibilityActions); + } } } } diff --git a/aplcore/src/primitives/functions.cpp b/aplcore/src/primitives/functions.cpp index 4935785..001bb51 100644 --- a/aplcore/src/primitives/functions.cpp +++ b/aplcore/src/primitives/functions.cpp @@ -24,6 +24,7 @@ #endif #include "apl/animation/coreeasing.h" +#include "apl/command/commandproperties.h" #include "apl/engine/context.h" #include "apl/primitives/rangegenerator.h" #include "apl/primitives/slicegenerator.h" @@ -238,13 +239,23 @@ arraySlice(const ObjectArray& args) static Object stringLength(const std::vector& args) { - if (args.size() < 1) + if (args.empty()) return Object::NULL_OBJECT(); std::string s = args[0].asString(); return utf8StringLength(s); } +static Object +stringCharAt(const std::vector& args) +{ + if (args.size() < 2) + return Object::NULL_OBJECT(); + + std::string s = args[0].asString(); + return utf8StringCharAt(args[0].asString(), args[1].asInt()); +} + static Object stringToUpperImpl(const std::shared_ptr& localeMethods, const std::vector& args) { @@ -352,6 +363,48 @@ timeFormat(const ObjectArray& args) return Object(timegrammar::timeToString(args.at(0).asString(), args.at(1).asNumber())); } +Object +logLevelName(const ObjectArray& args) +{ + if (args.size() != 1) + return Object::NULL_OBJECT(); + + auto level = args[0].asInt(); + if (!sCommandLogLevelMap.has(level)) + return Object::NULL_OBJECT(); + + return Object(sCommandLogLevelMap.at(level)); +} + +Object +logLevelValue(const ObjectArray& args) +{ + if (args.size() != 1) + return Object::NULL_OBJECT(); + + auto level = args[0].asString(); + if (!sCommandLogLevelMap.has(level)) + return Object::NULL_OBJECT(); + + return Object(sCommandLogLevelMap.at(level)); +} + +static ObjectMapPtr +createLogMap() +{ + auto map = std::make_shared(); + + map->emplace("levelName", Function::create("levelName", logLevelName)); + map->emplace("levelValue", Function::create("levelValue", logLevelValue)); + map->emplace("DEBUG", kCommandLogLevelDebug); + map->emplace("INFO", kCommandLogLevelInfo); + map->emplace("WARN", kCommandLogLevelWarn); + map->emplace("ERROR", kCommandLogLevelError); + map->emplace("CRITICAL", kCommandLogLevelCritical); + + return map; +} + static ObjectMapPtr createMathMap() { @@ -433,6 +486,7 @@ createStringMap(const std::shared_ptr& localeMethods) })); map->emplace("slice", Function::create("slice", stringSlice)); map->emplace("length", Function::create("length", stringLength)); + map->emplace("charAt", Function::create("charAt", stringCharAt)); return map; } @@ -471,12 +525,14 @@ void createStandardFunctions(Context& context) { static auto sArrayFunctions = createArrayMap(); + static auto sLogFunctions = createLogMap(); static auto sMathFunctions = createMathMap(); // String functions are dependent on RootConfig locale methods auto sStringFunctions = createStringMap(context.getLocaleMethods()); static auto sTimeFunctions = createTimeMap(); context.putConstant("Array", sArrayFunctions); + context.putConstant("Log", sLogFunctions); context.putConstant("Math", sMathFunctions); context.putConstant("String", sStringFunctions); context.putConstant("Time", sTimeFunctions); diff --git a/aplcore/src/primitives/unicode.cpp b/aplcore/src/primitives/unicode.cpp index 370682f..b70fd1a 100644 --- a/aplcore/src/primitives/unicode.cpp +++ b/aplcore/src/primitives/unicode.cpp @@ -145,6 +145,25 @@ utf8StringSlice(const std::string& utf8String, int start, int end) return std::string((char *)startPtr, endPtr - startPtr); } +std::string +utf8StringCharAt(const std::string& utf8String, int index) +{ + auto len = utf8StringLength(utf8String); + if (len <= 0) + return ""; + + // Handle a negative starting offset. Note that we don't support multiple wraps + if (index < 0) + index += len; + + if (index < 0 || index >= len) + return ""; + + auto startPtr = utf8AdvanceCodePointsUnsafe((uint8_t*)utf8String.c_str(), index); + auto endPtr = utf8AdvanceCodePointsUnsafe(startPtr, 1); + return std::string((char *)startPtr, endPtr - startPtr); +} + /** * Internal method to check a single UTF-8 character and see if it appears in a * string of valid characters. This method assumes that the inputs are valid UTF-8 strings diff --git a/aplcore/src/scenegraph/layer.cpp b/aplcore/src/scenegraph/layer.cpp index 94b9a61..40204d8 100644 --- a/aplcore/src/scenegraph/layer.cpp +++ b/aplcore/src/scenegraph/layer.cpp @@ -90,6 +90,7 @@ Layer::debugCharacteristicString() const static const char *FIXED_NAMES[] = { "DO_NOT_CLIP_CHILDREN", // 1u << 0 "RENDER_ONLY", // 1u << 1 + "HAS_MEDIA", // 1u << 2 nullptr }; diff --git a/aplcore/src/scenegraph/utilities.cpp b/aplcore/src/scenegraph/utilities.cpp index 0154bfd..dae5372 100644 --- a/aplcore/src/scenegraph/utilities.cpp +++ b/aplcore/src/scenegraph/utilities.cpp @@ -109,7 +109,7 @@ splitFontString(const RootConfig& rootConfig, const SessionPtr& session, const s } // Append the default font from the root config if it is not already at the end of the list - std::string defaultFont = rootConfig.getDefaultFontFamily(); + std::string defaultFont = rootConfig.getProperty(RootProperty::kDefaultFontFamily).getString(); if (state.strings.empty() || state.strings.back() != defaultFont) state.strings.emplace_back(defaultFont); diff --git a/aplcore/src/time/sequencer.cpp b/aplcore/src/time/sequencer.cpp index d8140e0..dce8116 100644 --- a/aplcore/src/time/sequencer.cpp +++ b/aplcore/src/time/sequencer.cpp @@ -201,7 +201,7 @@ Sequencer::terminate() if (DEBUG_SEQUENCER) { LOG(LogLevel::kDebug) << "Sequencer terminate"; - for (auto t : mSequencers) + for (const auto& t : mSequencers) LOG(LogLevel::kDebug) << "Thread: " << t.first; LOG(LogLevel::kDebug) << "OneShots: " << mOneShotSet.size(); diff --git a/aplcore/src/touch/gesture.cpp b/aplcore/src/touch/gesture.cpp index 63d02aa..5cb5cf6 100644 --- a/aplcore/src/touch/gesture.cpp +++ b/aplcore/src/touch/gesture.cpp @@ -49,7 +49,8 @@ static const std::map sGestureFunctions = }; std::shared_ptr -Gesture::create(const ActionablePtr& actionable, const Object& object) { +Gesture::create(const ActionablePtr& actionable, const Object& object) +{ if (!object.isMap()) return nullptr; @@ -83,7 +84,8 @@ Gesture::reset() } bool -Gesture::consume(const PointerEvent& event, apl_time_t timestamp) { +Gesture::consume(const PointerEvent& event, apl_time_t timestamp) +{ switch(event.pointerEventType) { case PointerEventType::kPointerDown: mStarted = true; @@ -110,16 +112,25 @@ Gesture::consume(const PointerEvent& event, apl_time_t timestamp) { } void -Gesture::passPointerEventThrough(const PointerEvent& event) { +Gesture::passPointerEventThrough(const PointerEvent& event) +{ Point localPoint = mActionable->toLocalPoint(event.pointerEventPosition); mActionable->executePointerEventHandler(sEventHandlers.at(event.pointerEventType), localPoint); } Point -Gesture::toLocalVector(const Point& vector) { +Gesture::toLocalVector(const Point& vector) +{ // Convert the vector to local space. Because the vector starts at (0,0) in local space, remove // the translation to avoid over-compensating for the position. return aboutOrigin(mActionable->getGlobalToLocalTransform()) * vector; } +const std::vector& +Gesture::getAccessibilityActions() const +{ + static std::vector sEmptyList = {}; + return sEmptyList; +} + } // namespace apl diff --git a/aplcore/src/touch/gestures/doublepressgesture.cpp b/aplcore/src/touch/gestures/doublepressgesture.cpp index 147d3c5..1493119 100644 --- a/aplcore/src/touch/gestures/doublepressgesture.cpp +++ b/aplcore/src/touch/gestures/doublepressgesture.cpp @@ -18,12 +18,14 @@ #include "apl/component/touchablecomponent.h" #include "apl/content/rootconfig.h" #include "apl/engine/propdef.h" +#include "apl/primitives/accessibilityaction.h" #include "apl/utils/session.h" namespace apl { std::shared_ptr -DoublePressGesture::create(const ActionablePtr& actionable, const Context& context, const Object& object) { +DoublePressGesture::create(const ActionablePtr& actionable, const Context& context, const Object& object) +{ Object onDoublePress = arrayifyPropertyAsObject(context, object, "onDoublePress"); Object onSinglePress = arrayifyPropertyAsObject(context, object, "onSinglePress"); @@ -36,10 +38,12 @@ DoublePressGesture::DoublePressGesture(const ActionablePtr& actionable, Object&& mOnSinglePress(std::move(onSinglePress)), mStartTime(0), mBetweenPresses(false), - mDoublePressTimeout(actionable->getRootConfig().getDoublePressTimeout()) {} + mDoublePressTimeout(actionable->getRootConfig().getProperty(RootProperty::kDoublePressTimeout).getDouble()) +{} void -DoublePressGesture::reset() { +DoublePressGesture::reset() +{ Gesture::reset(); mBetweenPresses = false; } @@ -47,7 +51,7 @@ DoublePressGesture::reset() { bool DoublePressGesture::invokeAccessibilityAction(const std::string& name) { - if (name == "doubletap") { + if (name == AccessibilityAction::ACCESSIBILITY_ACTION_DOUBLETAP) { mActionable->executeEventHandler("DoublePress", mOnDoublePress, false, mActionable->createTouchEventProperties(Point())); return true; @@ -55,8 +59,16 @@ DoublePressGesture::invokeAccessibilityAction(const std::string& name) return false; } +const std::vector& +DoublePressGesture::getAccessibilityActions() const +{ + static std::vector sActionsList = {AccessibilityAction::ACCESSIBILITY_ACTION_DOUBLETAP}; + return sActionsList; +} + void -DoublePressGesture::onFirstUpInternal(const PointerEvent& event, apl_time_t timestamp) { +DoublePressGesture::onFirstUpInternal(const PointerEvent& event, apl_time_t timestamp) +{ mBetweenPresses = true; if (timestamp >= mStartTime + mDoublePressTimeout) { @@ -72,14 +84,16 @@ DoublePressGesture::onFirstUpInternal(const PointerEvent& event, apl_time_t time } void -DoublePressGesture::onSecondDownInternal(const PointerEvent& event, apl_time_t timestamp) { +DoublePressGesture::onSecondDownInternal(const PointerEvent& event, apl_time_t timestamp) +{ mBetweenPresses = false; // Pass through passPointerEventThrough(event); } void -DoublePressGesture::onSecondUpInternal(const PointerEvent& event, apl_time_t timestamp) { +DoublePressGesture::onSecondUpInternal(const PointerEvent& event, apl_time_t timestamp) +{ Point localPoint = mActionable->toLocalPoint(event.pointerEventPosition); // Send cancel as we found double press at this point @@ -93,7 +107,8 @@ DoublePressGesture::onSecondUpInternal(const PointerEvent& event, apl_time_t tim } bool -DoublePressGesture::onDown(const PointerEvent& event, apl_time_t timestamp) { +DoublePressGesture::onDown(const PointerEvent& event, apl_time_t timestamp) +{ mStartTime = timestamp; if (mBetweenPresses) { onSecondDownInternal(event, timestamp); @@ -102,7 +117,8 @@ DoublePressGesture::onDown(const PointerEvent& event, apl_time_t timestamp) { } bool -DoublePressGesture::onTimeUpdate(const apl::PointerEvent& event, apl::apl_time_t timestamp) { +DoublePressGesture::onTimeUpdate(const apl::PointerEvent& event, apl::apl_time_t timestamp) +{ // Will only do something when in between presses if (timestamp >= mStartTime + mDoublePressTimeout && mBetweenPresses) { Point localPoint = mActionable->toLocalPoint(event.pointerEventPosition); @@ -114,7 +130,8 @@ DoublePressGesture::onTimeUpdate(const apl::PointerEvent& event, apl::apl_time_t } bool -DoublePressGesture::onUp(const PointerEvent& event, apl_time_t timestamp) { +DoublePressGesture::onUp(const PointerEvent& event, apl_time_t timestamp) +{ if (mTriggered) { onSecondUpInternal(event, timestamp); } else { diff --git a/aplcore/src/touch/gestures/flinggesture.cpp b/aplcore/src/touch/gestures/flinggesture.cpp index f466405..cb537fb 100644 --- a/aplcore/src/touch/gestures/flinggesture.cpp +++ b/aplcore/src/touch/gestures/flinggesture.cpp @@ -75,7 +75,8 @@ bool FlingGesture::passedScrollOrTapTimeout(apl_time_t timestamp) { // Wait for tap or scroll timeout to start checking for moves - return (timestamp - mStartTime) >= mActionable->getRootConfig().getTapOrScrollTimeout(); + return (timestamp - mStartTime) >= + mActionable->getRootConfig().getProperty(RootProperty::kTapOrScrollTimeout).getDouble(); } bool @@ -86,7 +87,7 @@ FlingGesture::isSlopeWithinTolerance(Point localPosition, bool horizontal) } auto pointerDelta = localPosition - mStartPosition; - auto maxSlope = mActionable->getRootConfig().getSwipeAngleSlope(); + auto maxSlope = mActionable->getRootConfig().getProperty(RootProperty::kSwipeAngleTolerance).getDouble(); if (horizontal) { return std::abs(pointerDelta.getX()) * maxSlope >= std::abs(pointerDelta.getY()); } else { diff --git a/aplcore/src/touch/gestures/longpressgesture.cpp b/aplcore/src/touch/gestures/longpressgesture.cpp index 1ca5731..ca1ae1a 100644 --- a/aplcore/src/touch/gestures/longpressgesture.cpp +++ b/aplcore/src/touch/gestures/longpressgesture.cpp @@ -18,6 +18,7 @@ #include "apl/component/touchwrappercomponent.h" #include "apl/content/rootconfig.h" #include "apl/engine/propdef.h" +#include "apl/primitives/accessibilityaction.h" #include "apl/utils/session.h" namespace apl { @@ -35,12 +36,12 @@ LongPressGesture::LongPressGesture(const ActionablePtr& actionable, Object&& onL mStartTime(0), mOnLongPressStart(std::move(onLongPressStart)), mOnLongPressEnd(std::move(onLongPressEnd)), - mLongPressTimeout(actionable->getRootConfig().getLongPressTimeout()) {} + mLongPressTimeout(actionable->getRootConfig().getProperty(RootProperty::kLongPressTimeout).getDouble()) {} bool LongPressGesture::invokeAccessibilityAction(const std::string& name) { - if (name == "longpress") { + if (name == AccessibilityAction::ACCESSIBILITY_ACTION_LONGPRESS) { mActionable->executeEventHandler("LongPressEnd", mOnLongPressEnd, false, mActionable->createTouchEventProperties(Point())); return true; @@ -48,14 +49,23 @@ LongPressGesture::invokeAccessibilityAction(const std::string& name) return false; } +const std::vector& +LongPressGesture::getAccessibilityActions() const +{ + static std::vector sActionsList = {AccessibilityAction::ACCESSIBILITY_ACTION_LONGPRESS}; + return sActionsList; +} + bool -LongPressGesture::onDown(const PointerEvent& event, apl_time_t timestamp) { +LongPressGesture::onDown(const PointerEvent& event, apl_time_t timestamp) +{ mStartTime = timestamp; return true; } bool -LongPressGesture::onTimeUpdate(const PointerEvent& event, apl_time_t timestamp) { +LongPressGesture::onTimeUpdate(const PointerEvent& event, apl_time_t timestamp) +{ if (!mTriggered && timestamp >= mStartTime + mLongPressTimeout) { Point localPoint = mActionable->toLocalPoint(event.pointerEventPosition); mActionable->executePointerEventHandler(kPropertyOnCancel, localPoint); @@ -68,7 +78,8 @@ LongPressGesture::onTimeUpdate(const PointerEvent& event, apl_time_t timestamp) } bool -LongPressGesture::onUp(const PointerEvent& event, apl_time_t timestamp) { +LongPressGesture::onUp(const PointerEvent& event, apl_time_t timestamp) +{ if (mTriggered) { Point localPoint = mActionable->toLocalPoint(event.pointerEventPosition); auto params = mActionable->createTouchEventProperties(localPoint); diff --git a/aplcore/src/touch/gestures/pagerflinggesture.cpp b/aplcore/src/touch/gestures/pagerflinggesture.cpp index 60ffba8..3a34f3b 100644 --- a/aplcore/src/touch/gestures/pagerflinggesture.cpp +++ b/aplcore/src/touch/gestures/pagerflinggesture.cpp @@ -166,7 +166,8 @@ PagerFlingGesture::onMove(const PointerEvent& event, apl_time_t timestamp) auto horizontal = mActionable->isHorizontal(); auto availableDirection = pager->pageDirection(); auto globalToLocal = mActionable->getGlobalToLocalTransform(); - auto flingDistanceThreshold = mActionable->getRootConfig().getPointerSlopThreshold() * + auto flingDistanceThreshold = + mActionable->getRootConfig().getProperty(RootProperty::kPointerSlopThreshold).getDouble() * (horizontal ? globalToLocal.getXScaling() : globalToLocal.getYScaling()); // Trigger only if distance is above the threshold AND navigation direction available @@ -222,7 +223,7 @@ PagerFlingGesture::onMove(const PointerEvent& event, apl_time_t timestamp) void PagerFlingGesture::animateRemainder(bool fulfill) { - auto duration = mActionable->getRootConfig().getDefaultPagerAnimationDuration(); + auto duration = mActionable->getRootConfig().getProperty(RootProperty::kDefaultPagerAnimationDuration).getDouble(); auto remainder = fulfill ? 1.0f - mAmount : -mAmount; std::weak_ptr weak_ptr(std::static_pointer_cast(shared_from_this())); @@ -313,7 +314,8 @@ PagerFlingGesture::finishUp() auto velocities = toLocalVector(mVelocityTracker->getEstimatedVelocity()); auto velocity = horizontal ? velocities.getX() : velocities.getY(); - auto minFlingVelocity = std::abs(mActionable->getRootConfig().getMinimumFlingVelocity() * scaleFactor); + auto minFlingVelocity = + std::abs(mActionable->getRootConfig().getProperty(RootProperty::kMinimumFlingVelocity).asFloat() * scaleFactor); auto fulfill = true; auto direction = (mActionable->isHorizontal() && mLayoutDirection == kLayoutDirectionRTL) ? (velocity < 0 ? kPageDirectionBack : kPageDirectionForward) diff --git a/aplcore/src/touch/gestures/scrollgesture.cpp b/aplcore/src/touch/gestures/scrollgesture.cpp index 7ab808a..64a4e34 100644 --- a/aplcore/src/touch/gestures/scrollgesture.cpp +++ b/aplcore/src/touch/gestures/scrollgesture.cpp @@ -90,7 +90,8 @@ ScrollGesture::onMove(const PointerEvent& event, apl_time_t timestamp) if (!isTriggered()) { auto triggerDistance = scrollable->isHorizontal() ? delta.getX() : delta.getY(); - auto flingTriggerDistanceThreshold = toLocalThreshold(scrollable->getRootConfig().getPointerSlopThreshold()); + auto flingTriggerDistanceThreshold = + toLocalThreshold(scrollable->getRootConfig().getProperty(RootProperty::kPointerSlopThreshold).getDouble()); if (std::abs(triggerDistance) > flingTriggerDistanceThreshold) { if (!isSlopeWithinTolerance(position)) { reset(); @@ -176,8 +177,8 @@ ScrollGesture::onUp(const PointerEvent& event, apl_time_t timestamp) // TODO: Acting on "significant" direction. Will will need this to be changed when we have // multidirectional scrolling. const auto& rootConfig = scrollable->getRootConfig(); - if (std::abs(velocity) >= toLocalThreshold(rootConfig.getMinimumFlingVelocity()) / - time::MS_PER_SECOND) { + if (std::abs(velocity) >= toLocalThreshold(rootConfig.getProperty(RootProperty::kMinimumFlingVelocity).getDouble()) / + time::MS_PER_SECOND) { auto position = scrollable->toLocalPoint(event.pointerEventPosition); auto delta = position - mStartPosition; auto velocityLimit = getVelocityLimit(delta); @@ -237,7 +238,7 @@ ScrollGesture::scrollToSnap() if (self) { self->reset(); } - }, snapOffset, rootConfig.getScrollSnapDuration()); + }, snapOffset, rootConfig.getProperty(RootProperty::kScrollSnapDuration).getDouble()); } float diff --git a/aplcore/src/touch/gestures/swipeawaygesture.cpp b/aplcore/src/touch/gestures/swipeawaygesture.cpp index aef3df0..4a6399e 100644 --- a/aplcore/src/touch/gestures/swipeawaygesture.cpp +++ b/aplcore/src/touch/gestures/swipeawaygesture.cpp @@ -24,6 +24,7 @@ #include "apl/engine/builder.h" #include "apl/engine/evaluate.h" #include "apl/engine/propdef.h" +#include "apl/primitives/accessibilityaction.h" #include "apl/primitives/timefunctions.h" #include "apl/primitives/transform.h" #include "apl/time/timemanager.h" @@ -80,7 +81,7 @@ SwipeAwayGesture::SwipeAwayGesture(const ActionablePtr& actionable, SwipeAwayAct mItems(std::move(items)), mLocalDistance(0), mTraveledDistance(0), - mAnimationEasing(actionable->getRootConfig().getSwipeAwayAnimationEasing()), + mAnimationEasing(actionable->getRootConfig().getProperty(RootProperty::kSwipeAwayAnimationEasing).get()), mInitialMove(0) {} void @@ -105,7 +106,7 @@ SwipeAwayGesture::reset() bool SwipeAwayGesture::invokeAccessibilityAction(const std::string& name) { - if (name == "swipeaway") { + if (name == AccessibilityAction::ACCESSIBILITY_ACTION_SWIPEAWAY) { mActionable->executeEventHandler("SwipeDone", mOnSwipeDone, false, mActionable->createTouchEventProperties(Point())); return true; @@ -113,6 +114,12 @@ SwipeAwayGesture::invokeAccessibilityAction(const std::string& name) return false; } +const std::vector& +SwipeAwayGesture::getAccessibilityActions() const +{ + static std::vector sActionsList = {AccessibilityAction::ACCESSIBILITY_ACTION_SWIPEAWAY}; + return sActionsList; +} float SwipeAwayGesture::getMove(SwipeDirection direction, Point localPos) @@ -135,14 +142,14 @@ SwipeAwayGesture::getMove(SwipeDirection direction, Point localPos) } } -int +float SwipeAwayGesture::getFulfillMoveDirection() { if (mDirection == SwipeDirection::kSwipeDirectionLeft || mDirection == SwipeDirection::kSwipeDirectionUp) - return -1; + return -1.0f; - return 1; + return 1.0f; } bool @@ -280,7 +287,8 @@ SwipeAwayGesture::onMove(const PointerEvent& event, apl_time_t timestamp) processTransformChange(travelPercentage); } } else { - if (move - mInitialMove > toLocalThreshold(mActionable->getRootConfig().getPointerSlopThreshold())) { + if (move - mInitialMove > + toLocalThreshold(mActionable->getRootConfig().getProperty(RootProperty::kPointerSlopThreshold).getDouble())) { if (!isSlopeWithinTolerance(localPoint, mDirection == kSwipeDirectionLeft || mDirection == kSwipeDirectionRight)) { reset(); return false; @@ -329,8 +337,8 @@ SwipeAwayGesture::animateRemainder(bool fulfilled, float velocity) apl_duration_t animationDuration = velocity > 0 ? std::abs(remainingDistance) / velocity - : rootConfig.getDefaultSwipeAnimationDuration(); - animationDuration = std::min(animationDuration, rootConfig.getMaxSwipeAnimationDuration()); + : rootConfig.getProperty(RootProperty::kDefaultSwipeAnimationDuration).getDouble(); + animationDuration = std::min(animationDuration, rootConfig.getProperty(RootProperty::kMaxSwipeAnimationDuration).getDouble()); // Ensure "Early exit", if (remainingDistance == 0) { @@ -395,13 +403,15 @@ bool SwipeAwayGesture::swipedFarEnough() { return mTraveledDistance / mLocalDistance - >= mActionable->getRootConfig().getSwipeAwayFulfillDistancePercentageThreshold(); + >= mActionable->getRootConfig().getProperty(RootProperty::kSwipeAwayFulfillDistancePercentageThreshold).getDouble(); } bool SwipeAwayGesture::swipedFastEnough(float velocity) { - auto swipeVelocityThreshold = mActionable->getRootConfig().getSwipeVelocityThreshold()/time::MS_PER_SECOND; + auto swipeVelocityThreshold = + mActionable->getRootConfig(). + getProperty(RootProperty::kSwipeVelocityThreshold).asFloat() / time::MS_PER_SECOND; auto velocityThreshold = toLocalThreshold(swipeVelocityThreshold); // Check for velocity condition return (getFulfillMoveDirection() * velocity > 0) && (std::abs(velocity) >= velocityThreshold); @@ -428,7 +438,8 @@ SwipeAwayGesture::onUp(const PointerEvent& event, apl_time_t timestamp) { } else { // Limit to the configured maximum. We don't want speed of light here. velocity = std::min(std::abs(velocity), - mActionable->getRootConfig().getSwipeMaxVelocity()/time::MS_PER_SECOND); + mActionable->getRootConfig().getProperty(RootProperty::kSwipeMaxVelocity).asFloat() + / time::MS_PER_SECOND); } animateRemainder(finishUp, velocity); diff --git a/aplcore/src/touch/gestures/tapgesture.cpp b/aplcore/src/touch/gestures/tapgesture.cpp index 724d7ce..23810f7 100644 --- a/aplcore/src/touch/gestures/tapgesture.cpp +++ b/aplcore/src/touch/gestures/tapgesture.cpp @@ -20,7 +20,7 @@ #include "apl/component/touchablecomponent.h" #include "apl/content/rootconfig.h" #include "apl/engine/propdef.h" -#include "apl/primitives/timefunctions.h" +#include "apl/primitives/accessibilityaction.h" namespace apl { @@ -50,16 +50,28 @@ TapGesture::reset() { bool TapGesture::invokeAccessibilityAction(const std::string& name) { - if (name == "activate") { - mActionable->executeEventHandler("Tap", mOnTap, false, mActionable->createTouchEventProperties(Point())); + if (name == AccessibilityAction::ACCESSIBILITY_ACTION_ACTIVATE || + name == AccessibilityAction::ACCESSIBILITY_ACTION_TAP) { + mActionable->executeEventHandler( + "Tap", mOnTap, false, mActionable->createTouchEventProperties(Point())); return true; } return false; } +const std::vector& +TapGesture::getAccessibilityActions() const +{ + static std::vector sActionsList = { + AccessibilityAction::ACCESSIBILITY_ACTION_TAP, + AccessibilityAction::ACCESSIBILITY_ACTION_ACTIVATE}; + return sActionsList; +} + bool -TapGesture::onDown(const PointerEvent& event, apl_time_t timestamp) { +TapGesture::onDown(const PointerEvent& event, apl_time_t timestamp) +{ mStartTime = timestamp; mStartPoint = mActionable->toLocalPoint(event.pointerEventPosition); @@ -67,7 +79,8 @@ TapGesture::onDown(const PointerEvent& event, apl_time_t timestamp) { } bool -TapGesture::onUp(const PointerEvent& event, apl_time_t timestamp) { +TapGesture::onUp(const PointerEvent& event, apl_time_t timestamp) +{ float elapsedTimeInSeconds = (timestamp - mStartTime) / 1000; Point endPoint = mActionable->toLocalPoint(event.pointerEventPosition); diff --git a/aplcore/src/touch/utils/unidirectionaleasingscroller.cpp b/aplcore/src/touch/utils/unidirectionaleasingscroller.cpp index 3cb80dc..15dbe64 100644 --- a/aplcore/src/touch/utils/unidirectionaleasingscroller.cpp +++ b/aplcore/src/touch/utils/unidirectionaleasingscroller.cpp @@ -67,7 +67,8 @@ UnidirectionalEasingScroller::make( // 0 duration means go to it directly. if (duration == 0) { - auto resultingPosition = scrollable->isVertical() ? target.getY() : target.getX(); + auto endPosition = scrollable->scrollPosition() + target; + auto resultingPosition = scrollable->isVertical() ? endPosition.getY() : endPosition.getX(); scrollable->update(UpdateType::kUpdateScrollPosition, resultingPosition); finish(); return nullptr; diff --git a/aplcore/src/touch/utils/velocitytracker.cpp b/aplcore/src/touch/utils/velocitytracker.cpp index 0029b1b..02911d0 100644 --- a/aplcore/src/touch/utils/velocitytracker.cpp +++ b/aplcore/src/touch/utils/velocitytracker.cpp @@ -98,7 +98,8 @@ class FilterVelocityEstimationStrategy : public VelocityEstimationStrategy { ////////////////////////////////////////////////////////////////////////////////////////// VelocityTracker::VelocityTracker(const RootConfig& rootConfig) - : mLastEventTimestamp(0), mPointerInactivityTimeout(rootConfig.getPointerInactivityTimeout()) { + : mLastEventTimestamp(0), + mPointerInactivityTimeout(rootConfig.getProperty(RootProperty::kPointerInactivityTimeout).getDouble()) { // TODO: Basic case here is something that is trivial filter preferring later velocity. // Need to make strategies configurable. mEstimationStrategy = std::make_shared(); diff --git a/aplcore/unit/audio/testaudioplayer.cpp b/aplcore/unit/audio/testaudioplayer.cpp index 8878728..ada30e1 100644 --- a/aplcore/unit/audio/testaudioplayer.cpp +++ b/aplcore/unit/audio/testaudioplayer.cpp @@ -70,6 +70,23 @@ TestAudioPlayer::release() mActionRef.resolve(); } + +bool TextTracksEqual(TextTrackArray track1, TextTrackArray track2){ + + if (track1.size() != track2.size()) { + return false; + } + + for (int i = 0; i < track1.size(); i++) { + // Two TextTracks are the same if they have the same source url, type and size + if (track1.at(i).type != track2.at(i).type || track1.at(i).url != track2.at(i).url) { + return false; + } + } + + return true; +} + void TestAudioPlayer::setTrack(MediaTrack track) { @@ -77,7 +94,6 @@ TestAudioPlayer::setTrack(MediaTrack track) return; LOG_IF(DEBUG_TEST_AUDIO_PLAYER) << "track.url=" << track.url; - pause(); if (track.url.empty()) @@ -85,6 +101,7 @@ TestAudioPlayer::setTrack(MediaTrack track) auto content = mFactory->findContent(track.url); + assert(TextTracksEqual(track.textTracks, content.trackArray)); assert(content.initialDelay> 0); assert(content.actualDuration > 0); diff --git a/aplcore/unit/audio/testaudioplayerfactory.h b/aplcore/unit/audio/testaudioplayerfactory.h index 33faa58..b6766f6 100644 --- a/aplcore/unit/audio/testaudioplayerfactory.h +++ b/aplcore/unit/audio/testaudioplayerfactory.h @@ -33,6 +33,7 @@ struct FakeAudioContent { int initialDelay; // Initial buffering delay in milliseconds. This applies to failed tracks as well. int failAfter; // Fail after this many milliseconds. May be 0. Negative numbers never fail. std::vector speechMarks; // Ordered series of speech marks to send out + TextTrackArray trackArray; // TextTrack (Caption) data associated with the Track }; class TestAudioPlayerFactory : public AudioPlayerFactory, diff --git a/aplcore/unit/audio/unittest_speak_item_audio.cpp b/aplcore/unit/audio/unittest_speak_item_audio.cpp index 485e3b4..167c40d 100644 --- a/aplcore/unit/audio/unittest_speak_item_audio.cpp +++ b/aplcore/unit/audio/unittest_speak_item_audio.cpp @@ -674,6 +674,7 @@ TEST_F(SpeakItemAudioTest, MissingSpeech) ASSERT_TRUE(CheckDirty(child, kPropertyColor, kPropertyColorKaraokeTarget, kPropertyVisualHash)); // Color change ASSERT_TRUE(CheckDirty(root, child)); ASSERT_EQ(Object(Color(Color::GREEN)), child->getCalculated(kPropertyColor)); + ASSERT_TRUE(ConsoleMessage()); } /** @@ -697,6 +698,7 @@ TEST_F(SpeakItemAudioTest, MissingSpeechNoDwell) // At this point nothing should be left - without a dwell time or speech, we don't get a change ASSERT_FALSE(root->hasEvent()); // No events pending ASSERT_TRUE(CheckDirty(root)); + ASSERT_TRUE(ConsoleMessage()); } static const char * MISSING_SPEECH_AND_SCROLL = R"apl( @@ -763,6 +765,7 @@ TEST_F(SpeakItemAudioTest, MissingSpeechAndScroll) ASSERT_TRUE(CheckDirty(child, kPropertyColor, kPropertyColorKaraokeTarget, kPropertyVisualHash)); // Color change ASSERT_TRUE(CheckDirty(root, child)); ASSERT_EQ(Object(Color(Color::GREEN)), child->getCalculated(kPropertyColor)); + ASSERT_TRUE(ConsoleMessage()); } /** @@ -779,6 +782,7 @@ TEST_F(SpeakItemAudioTest, MissingSpeechAndScrollNoDwell) // Nothing should happen ASSERT_FALSE(root->hasEvent()); // No events pending ASSERT_TRUE(CheckDirty(root)); + ASSERT_TRUE(ConsoleMessage()); } static const char *MISSING_SCROLL = R"apl( @@ -1629,3 +1633,139 @@ TEST_F(SpeakItemAudioTest, MarksAfterText) ASSERT_EQ(-1, event.getValue(apl::kEventPropertyRangeStart).getInteger()); ASSERT_EQ(-1, event.getValue(apl::kEventPropertyRangeEnd).getInteger()); } + +static const char *TEST_STAGES_TEXTTRACK = R"apl( +{ + "type": "APL", + "version": "1.1", + "styles": { + "base": { + "values": [ + { + "color": "green" + }, + { + "when": "${state.karaoke}", + "color": "blue" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "ScrollView", + "width": 500, + "height": 300, + "item": { + "type": "Container", + "items": { + "type": "Text", + "style": "base", + "text": "${data.item}", + "speech": "${data.speech}", + "height": 200 + }, + "data": [ + { + "item" : "URL1", + "speech" : { + "url": "URL1", + "textTrack" : { + "content" : "http://URL1", + "type" : "caption" + } + } + }, + { + "item" : "URL2", + "speech" : "URL2" + }, + { + "item" : "URL3", + "speech" : { + "url": "URL3", + "textTrack" : { + "content" : "http://URL3", + "type" : "caption" + } + } + }, + { + "item" : "URL4", + "speech" : { + "url": "URL4", + "textTrack" : { + "content" : "http://URL4", + "type" : "caption" + } + } + } + ] + } + } + } +} +)apl"; + +TEST_F(SpeakItemAudioTest, TestStagesCaption) +{ + factory->addFakeContent({ + {"URL1", 1000, 100, -1, {}}, // 1000 ms duration, 100 ms initial delay + {"URL2", 1000, 100, -1, {}}, // 1000 ms duration, 100 ms initial delay + {"URL3", 1000, 100, -1, {}}, // 1000 ms duration, 100 ms initial delay + {"URL4", 1000, 100, -1, {}}, // 1000 ms duration, 100 ms initial delay + }); + // Set how long it takes to scroll + config->set(RootProperty::kScrollCommandDuration, 200); + + loadDocument(TEST_STAGES_TEXTTRACK); + auto container = component->getChildAt(0); + auto child = container->getChildAt(1); + + ASSERT_EQ(Object(Color(Color::GREEN)), child->getCalculated(kPropertyColor)); + + executeSpeakItem(child, kCommandScrollAlignFirst, kCommandHighlightModeBlock, 1000); + + // The first thing we should get is a pre-roll event + ASSERT_TRUE(CheckPlayer("URL2", TestAudioPlayer::kPreroll)); + ASSERT_FALSE(factory->hasEvent()); + ASSERT_FALSE(root->hasEvent()); + + // Step forward 100 ms. This takes us past the loading delay, and into the middle of our scrolling + advanceTime(100); + ASSERT_TRUE(CheckPlayer("URL2", TestAudioPlayer::kReady)); + ASSERT_FALSE(factory->hasEvent()); + + ASSERT_EQ(Point(0, 100), component->scrollPosition()); // Halfway through scrolling + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged, kPropertyScrollPosition)); + ASSERT_TRUE(CheckDirty(root, component)); + + // Step forward 100 ms. This finishes the scrolling and kicks off the speech command + advanceTime(100); + ASSERT_TRUE(CheckPlayer("URL2", TestAudioPlayer::kPlay)); + ASSERT_FALSE(factory->hasEvent()); + + ASSERT_EQ(Point(0, 200), component->scrollPosition()); // Finished scrolling + ASSERT_EQ(Object(Color(Color::BLUE)), child->getCalculated(kPropertyColor)); + + ASSERT_TRUE(CheckDirty(child, kPropertyColor, kPropertyColorKaraokeTarget, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged, kPropertyScrollPosition)); + ASSERT_TRUE(CheckDirty(root, component, child)); + + // Step forward another 500 ms. We should still be speaking - nothing visually has changed + advanceTime(500); + ASSERT_FALSE(factory->hasEvent()); + ASSERT_TRUE(CheckDirty(root)); + + // Another 500 ms takes us to the end of speech. Everything changes back + advanceTime(500); + ASSERT_TRUE(CheckPlayer("URL2", TestAudioPlayer::kDone)); + ASSERT_TRUE(CheckPlayer("URL2", TestAudioPlayer::kRelease)); + ASSERT_FALSE(factory->hasEvent()); + + ASSERT_EQ(Object(Color(Color::GREEN)), child->getCalculated(kPropertyColor)); + + ASSERT_TRUE(CheckDirty(child, kPropertyColor, kPropertyColorKaraokeTarget, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(root, child)); +} \ No newline at end of file diff --git a/aplcore/unit/audio/unittest_speak_list_audio.cpp b/aplcore/unit/audio/unittest_speak_list_audio.cpp index c5a6aba..e830d55 100644 --- a/aplcore/unit/audio/unittest_speak_list_audio.cpp +++ b/aplcore/unit/audio/unittest_speak_list_audio.cpp @@ -741,3 +741,391 @@ TEST_F(SpeakListAudioTest, PreserveShortenedList) ASSERT_FALSE(factory->hasEvent()); } + +static const char *TEST_STAGES_TEXTTRACK = R"apl( +{ + "type": "APL", + "version": "1.1", + "styles": { + "base": { + "values": [ + { + "color": "green" + }, + { + "when": "${state.karaoke}", + "color": "blue" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "ScrollView", + "width": 500, + "height": 300, + "item": { + "type": "Container", + "items": { + "type": "Text", + "style": "base", + "text": "${data.item}", + "speech": "${data.speech}", + "height": 200 + }, + "data": [ + { + "item" : "URL1", + "speech" : { + "url": "http://URL1", + "textTrack" : { + "content" : "http://URL1", + "type" : "caption" + } + } + }, + { + "item" : "URL2", + "speech" : "http://URL2" + }, + { + "item" : "URL3", + "speech" : { + "url": "http://URL3", + "textTrack" : { + "content" : "http://URL3", + "type" : "caption" + } + } + }, + { + "item" : "URL4", + "speech" : { + "url": "http://URL4", + "textTrack" : { + "content" : "http://URL4", + "type" : "caption" + } + } + } + ] + } + } + } +} +)apl"; + +TEST_F(SpeakListAudioTest, TestStagesCaption) +{ + factory->addFakeContent({ + {"http://URL1", 2000, 100, -1, {}, {{kTextTrackTypeCaption,"http://URL1", ""}}}, // 2000 ms duration, 100 ms initial delay + {"http://URL2", 2000, 100, -1, {}, {}}, + {"http://URL3", 2000, 100, -1, {}, {{kTextTrackTypeCaption,"http://URL3", ""}}}, + {"http://URL4", 2000, 100, -1, {}, {{kTextTrackTypeCaption,"http://URL4", ""}}}, + }); + + + // Set how long it takes to scroll and make sure that scrolling is linear + config->set(RootProperty::kScrollCommandDuration, 200); + config->set(RootProperty::kUEScrollerDurationEasing, CoreEasing::linear()); + + loadDocument(TEST_STAGES_TEXTTRACK); + + auto container = component->getChildAt(0); + const int CHILD_COUNT = 4; + ASSERT_EQ(CHILD_COUNT, container->getChildCount()); + + // Check the starting colors + for (int i = 0 ; i < CHILD_COUNT ; i++) + ASSERT_EQ(Object(Color(Color::GREEN)), container->getChildAt(i)->getCalculated(kPropertyColor)); + + // Run speak list and pass a big number so we get everyone + executeSpeakList(container, // The scrollview + kCommandScrollAlignFirst, // Scroll to align at the top + kCommandHighlightModeBlock, // Block highlighting + 0, // Start + 100000, // Count + 1000, // Minimum dwell time + 500); // Delay + + // Nothing happens because of the delay (including no preroll) + ASSERT_FALSE(root->hasEvent()); + + // After the delay has passed, we should get a preroll and the scroll should start + advanceTime(500); + + for (int i = 0 ; i < CHILD_COUNT ; i++) + CheckScrollAndPlay(component, + container->getChildAt(i), + "http://URL"+std::to_string(i+1), + 100, // preroll duration + 200, // scroll duration + std::min(200 * i, 500), // scroll position + 2000, // play duration + "child["+std::to_string(i+1)+"]"); + + ASSERT_FALSE(factory->hasEvent()); + ASSERT_FALSE(root->hasEvent()); +} + +static const char *TEST_STAGES_TEXTTRACK_MISSING = R"apl( +{ + "type": "APL", + "version": "1.1", + "styles": { + "base": { + "values": [ + { + "color": "green" + }, + { + "when": "${state.karaoke}", + "color": "blue" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "ScrollView", + "width": 500, + "height": 300, + "item": { + "type": "Container", + "items": { + "type": "Text", + "style": "base", + "text": "${data.item}", + "speech": "${data.speech}", + "height": 200 + }, + "data": [ + { + "item" : "URL1", + "speech" : { + "url": "http://URL1", + "textTrack" : { + "type" : "caption" + } + } + }, + { + "item" : "URL2", + "speech" : "http://URL2" + }, + { + "item" : "URL3", + "speech" : { + "url": "http://URL3", + "textTrack" : { + "content" : "http://URL3" + } + } + }, + { + "item" : "URL4", + "speech" : { + "url": "http://URL4", + "textTrack" : { + } + } + } + ] + } + } + } +} +)apl"; + +TEST_F(SpeakListAudioTest, TestStagesCaptionMissing) +{ + factory->addFakeContent({ + // Missing TextTrack content but type included: Empty TextTrackArray + {"http://URL1", 2000, 100, -1, {}, {}}, + // Regular speech data without captions: Empty TextTrackArray + {"http://URL2", 2000, 100, -1, {}, {}}, + // Missing TextTrack type: Empty TextTrackArray + {"http://URL3", 2000, 100, -1, {}, {}}, + // Missing all TextTrack properties: Empty TextTrackArray + {"http://URL4", 2000, 100, -1, {}, {}}, + }); + + + // Set how long it takes to scroll and make sure that scrolling is linear + config->set(RootProperty::kScrollCommandDuration, 200); + config->set(RootProperty::kUEScrollerDurationEasing, CoreEasing::linear()); + + loadDocument(TEST_STAGES_TEXTTRACK_MISSING); + + auto container = component->getChildAt(0); + const int CHILD_COUNT = 4; + ASSERT_EQ(CHILD_COUNT, container->getChildCount()); + + // Check the starting colors + for (int i = 0 ; i < CHILD_COUNT ; i++) + ASSERT_EQ(Object(Color(Color::GREEN)), container->getChildAt(i)->getCalculated(kPropertyColor)); + + // Run speak list and pass a big number so we get everyone + executeSpeakList(container, // The scrollview + kCommandScrollAlignFirst, // Scroll to align at the top + kCommandHighlightModeBlock, // Block highlighting + 0, // Start + 100000, // Count + 1000, // Minimum dwell time + 500); // Delay + + // Nothing happens because of the delay (including no preroll) + ASSERT_FALSE(root->hasEvent()); + + // After the delay has passed, we should get a preroll and the scroll should start + advanceTime(500); + + for (int i = 0 ; i < CHILD_COUNT ; i++) + CheckScrollAndPlay(component, + container->getChildAt(i), + "http://URL"+std::to_string(i+1), + 100, // preroll duration + 200, // scroll duration + std::min(200 * i, 500), // scroll position + 2000, // play duration + "child["+std::to_string(i+1)+"]"); + + ASSERT_FALSE(factory->hasEvent()); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); +} + +static const char *TEST_STAGES_TEXTTRACK_INCORRECT = R"apl( +{ + "type": "APL", + "version": "1.1", + "styles": { + "base": { + "values": [ + { + "color": "green" + }, + { + "when": "${state.karaoke}", + "color": "blue" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "ScrollView", + "width": 500, + "height": 300, + "item": { + "type": "Container", + "items": { + "type": "Text", + "style": "base", + "text": "${data.item}", + "speech": "${data.speech}", + "height": 200 + }, + "data": [ + { + "item" : "URL1", + "speech" : { + "url": "http://URL1", + "textTrack" : { + "content": "http://URL1", + "type" : "subtitle" + } + } + }, + { + "item" : "URL2", + "speech" : { + "url": "http://URL2", + "textTrack" : { + "content" : "", + "type": "caption" + } + } + }, + { + "item" : "URL3", + "speech" : { + "url": "http://URL3", + "textTrack" : { + "content" : "http://URL3", + "type": "" + } + } + }, + { + "item" : "URL4", + "speech" : { + "url": "http://URL4", + "textTrack" : { + } + } + } + ] + } + } + } +} +)apl"; + +TEST_F(SpeakListAudioTest, TestStagesCaptionIncorrect) +{ + factory->addFakeContent({ + // Incorrect type of "subtitle": Empty TextTrackArray + {"http://URL1", 2000, 100, -1, {}, {}}, + // Incorrect content is empty: Empty TextTrackArray + {"http://URL2", 2000, 100, -1, {}, {}}, + // Incorrect type is empty: Empty TextTrackArray + {"http://URL3", 2000, 100, -1, {}, {}}, + // Missing all TextTrack properties: Empty TextTrackArray + {"http://URL4", 2000, 100, -1, {}, {}}, + }); + + + // Set how long it takes to scroll and make sure that scrolling is linear + config->set(RootProperty::kScrollCommandDuration, 200); + config->set(RootProperty::kUEScrollerDurationEasing, CoreEasing::linear()); + + loadDocument(TEST_STAGES_TEXTTRACK_INCORRECT); + + auto container = component->getChildAt(0); + const int CHILD_COUNT = 4; + ASSERT_EQ(CHILD_COUNT, container->getChildCount()); + + // Check the starting colors + for (int i = 0 ; i < CHILD_COUNT ; i++) + ASSERT_EQ(Object(Color(Color::GREEN)), container->getChildAt(i)->getCalculated(kPropertyColor)); + + // Run speak list and pass a big number so we get everyone + executeSpeakList(container, // The scrollview + kCommandScrollAlignFirst, // Scroll to align at the top + kCommandHighlightModeBlock, // Block highlighting + 0, // Start + 100000, // Count + 1000, // Minimum dwell time + 500); // Delay + + // Nothing happens because of the delay (including no preroll) + ASSERT_FALSE(root->hasEvent()); + + // After the delay has passed, we should get a preroll and the scroll should start + advanceTime(500); + + for (int i = 0 ; i < CHILD_COUNT ; i++) + CheckScrollAndPlay(component, + container->getChildAt(i), + "http://URL"+std::to_string(i+1), + 100, // preroll duration + 200, // scroll duration + std::min(200 * i, 500), // scroll position + 2000, // play duration + "child["+std::to_string(i+1)+"]"); + + ASSERT_FALSE(factory->hasEvent()); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); +} \ No newline at end of file diff --git a/aplcore/unit/command/CMakeLists.txt b/aplcore/unit/command/CMakeLists.txt index 6d8960c..e9845f6 100644 --- a/aplcore/unit/command/CMakeLists.txt +++ b/aplcore/unit/command/CMakeLists.txt @@ -19,6 +19,7 @@ target_sources_local(unittest unittest_command_document.cpp unittest_command_event_binding.cpp unittest_command_insertitem.cpp + unittest_command_log.cpp unittest_command_macros.cpp unittest_command_media.cpp unittest_command_openurl.cpp @@ -28,6 +29,8 @@ target_sources_local(unittest unittest_command_sendevent.cpp unittest_command_setvalue.cpp unittest_commands.cpp + unittest_commands_parallel.cpp + unittest_commands_sequential.cpp unittest_screenlock.cpp unittest_sequencer_preservation.cpp unittest_serialize_event.cpp @@ -35,4 +38,4 @@ target_sources_local(unittest unittest_setvalue.cpp unittest_speak_item.cpp unittest_speak_list.cpp - ) \ No newline at end of file + ) diff --git a/aplcore/unit/command/unittest_command_insertitem.cpp b/aplcore/unit/command/unittest_command_insertitem.cpp index d2a1c5a..fa0372c 100644 --- a/aplcore/unit/command/unittest_command_insertitem.cpp +++ b/aplcore/unit/command/unittest_command_insertitem.cpp @@ -49,7 +49,8 @@ class CommandInsertItemTest : public CommandTest { const CoreComponentPtr& target, const CoreComponentPtr& child, const int initialChildCount, - const int expectedIndex) { + const int expectedIndex, + const std::string& path) { ASSERT_FALSE(session->checkAndClear()); ASSERT_TRUE(root->isDirty()); @@ -59,8 +60,7 @@ class CommandInsertItemTest : public CommandTest { ASSERT_EQ(target->getChildIndex(child), expectedIndex); ASSERT_TRUE(CheckDirtyAtLeast(root, target, child)); ASSERT_EQ(child->getParent()->getId(), target->getId()); - ASSERT_EQ(child->getPathObject().toString(), "_virtual"); - ASSERT_EQ(child->getContext()->parent(), target->getContext()); + ASSERT_EQ(child->getPathObject().toString(), path); } void @@ -241,7 +241,7 @@ TEST_F(CommandInsertItemTest, InsertItemSkippingFalseWhenClause) }])"); auto child = CoreComponent::cast(root->findComponentById("newArrival")); - validateInsert(target, child, initialChildCount, 0); + validateInsert(target, child, initialChildCount, 0, "_main/mainTemplate/item/items/1"); ASSERT_FALSE(root->findComponentById("whenIsFalse")); ASSERT_FALSE(root->findComponentById("unreachable")); } @@ -275,7 +275,7 @@ TEST_F(CommandInsertItemTest, InsertItemWhenTargetAlreadyHasOnlyChild) executeInsertItem("alreadyHasAChild"); - validateNonInsert("Could not insert child into 'alreadyHasAChild'", target, initialChildCount); + validateNonInsert("Could not insert child into 'alreadyHasAChild'", target, initialChildCount, "_main/mainTemplate/item/items/1"); } TEST_F(CommandInsertItemTest, InsertItem) @@ -299,7 +299,7 @@ TEST_F(CommandInsertItemTest, InsertItem) })"); auto child = CoreComponent::cast(root->findComponentById("newArrival")); - validateInsert(target, child, initialChildCount, 0); + validateInsert(target, child, initialChildCount, 0, "_main/mainTemplate/item/items/1"); ASSERT_EQ(TextComponent::cast(child)->getValue().asString(), "blue"); } @@ -326,7 +326,7 @@ TEST_F(CommandInsertItemTest, InsertItems) false); auto child = CoreComponent::cast(root->findComponentById("newArrival")); - validateInsert(target, child, initialChildCount, 0); + validateInsert(target, child, initialChildCount, 0, "_main/mainTemplate/item/items/1"); } TEST_F(CommandInsertItemTest, InsertItemDefaultAtAppends) @@ -343,7 +343,7 @@ TEST_F(CommandInsertItemTest, InsertItemDefaultAtAppends) executeInsertItem("main",INT_MAX); auto child = CoreComponent::cast(root->findComponentById("newArrival")); - validateInsert(target, child, initialChildCount, initialChildCount); + validateInsert(target, child, initialChildCount, initialChildCount, "_main/mainTemplate/item"); } TEST_F(CommandInsertItemTest, InsertItemNegativeInsertsFromEnd) @@ -360,7 +360,7 @@ TEST_F(CommandInsertItemTest, InsertItemNegativeInsertsFromEnd) executeInsertItem("multiChild",-1); auto child = CoreComponent::cast(root->findComponentById("newArrival")); - validateInsert(target, child, initialChildCount, initialChildCount - 1); + validateInsert(target, child, initialChildCount, initialChildCount - 1, "_main/mainTemplate/item/items/3"); } TEST_F(CommandInsertItemTest, InsertItemNegativeWalksOffLeftEnd) @@ -377,7 +377,7 @@ TEST_F(CommandInsertItemTest, InsertItemNegativeWalksOffLeftEnd) executeInsertItem("multiChild", -1 * (initialChildCount + 1)); auto child = CoreComponent::cast(root->findComponentById("newArrival")); - validateInsert(target, child, initialChildCount, 0); + validateInsert(target, child, initialChildCount, 0, "_main/mainTemplate/item/items/3"); } TEST_F(CommandInsertItemTest, InsertItemMultiChildZeroIsBeforeFirstItem) @@ -394,7 +394,7 @@ TEST_F(CommandInsertItemTest, InsertItemMultiChildZeroIsBeforeFirstItem) executeInsertItem("multiChild"); auto child = CoreComponent::cast(root->findComponentById("newArrival")); - validateInsert(target, child, initialChildCount, 0); + validateInsert(target, child, initialChildCount, 0, "_main/mainTemplate/item/items/3"); ASSERT_EQ(target->getChildAt(1)->getId(), "firstChild"); } @@ -412,7 +412,7 @@ TEST_F(CommandInsertItemTest, InsertItemMultiChildAppendIsAfterLastItem) executeInsertItem("multiChild", INT_MAX); auto child = CoreComponent::cast(root->findComponentById("newArrival")); - validateInsert(target, child, initialChildCount, initialChildCount); + validateInsert(target, child, initialChildCount, initialChildCount, "_main/mainTemplate/item/items/3"); ASSERT_EQ(target->getChildAt(initialChildCount - 1)->getId(), "lastChild"); } @@ -450,4 +450,87 @@ TEST_F(CommandInsertItemTest, InsertItemWhenTargetUsesArrayDataInflation) executeInsertItem("main"); validateNonInsert("Could not insert child into 'main'", target, initialChildCount); -} \ No newline at end of file +} + +const char *BASE_SEQUENCE = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "items": { + "id": "baseContainer", + "type": "Sequence", + "items": [] + } + } +})"; + +const char *INSERT_TEXT_AT_THE_END = R"({ + "type": "Text", + "height": 200, + "width": "100%", + "text": "${index} ${length}" +})"; + +TEST_F(CommandInsertItemTest, InsertItemProperContextResolution) +{ + loadDocument(BASE_SEQUENCE); + + executeInsertItem("baseContainer", INT_MAX, INSERT_TEXT_AT_THE_END); + + ASSERT_EQ("0 1", component->getCoreChildAt(0)->getCalculated(apl::kPropertyText).asString()); + + executeInsertItem("baseContainer", INT_MAX, INSERT_TEXT_AT_THE_END); + + ASSERT_EQ("1 2", component->getCoreChildAt(1)->getCalculated(apl::kPropertyText).asString()); +} + +const char *BASE_SEQUENCE_WITH_PARAMETERS = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "layouts": { + "Bubble": { + "parameters": [{"name": "Content"}], + "items": { + "type": "Frame", + "item": { + "type": "Text", + "height": "auto", + "width": "100%", + "text": "${Content}" + } + } + } + }, + "mainTemplate": { + "parameters": ["TextBox"], + "items": { + "id": "baseContainer", + "type": "Sequence", + "height": 500, + "items": [] + } + } +})"; + +const char *INSERT_FRAMED_TEXT_AT_THE_END = R"({ + "type": "Bubble", + "Content": "${TextBox[index]}" +})"; + +TEST_F(CommandInsertItemTest, InsertItemProperParameterResolution) +{ + loadDocument(BASE_SEQUENCE_WITH_PARAMETERS, R"({ "TextBox": [ "Index 0", "Index 1" ] })"); + + executeInsertItem("baseContainer", INT_MAX, INSERT_FRAMED_TEXT_AT_THE_END); + + ASSERT_EQ("Index 0", component->getCoreChildAt(0)->getCoreChildAt(0)->getCalculated(apl::kPropertyText).asString()); + LOG(LogLevel::kError) << component->getCoreChildAt(0)->getCoreChildAt(0)->getCalculated(apl::kPropertyText).asString(); + + executeInsertItem("baseContainer", INT_MAX, INSERT_FRAMED_TEXT_AT_THE_END); + + ASSERT_EQ("Index 1", component->getCoreChildAt(1)->getCoreChildAt(0)->getCalculated(apl::kPropertyText).asString()); + LOG(LogLevel::kError) << component->getCoreChildAt(1)->getCoreChildAt(0)->getCalculated(apl::kPropertyText).asString(); + + root->clearPending(); +} diff --git a/aplcore/unit/command/unittest_command_log.cpp b/aplcore/unit/command/unittest_command_log.cpp new file mode 100644 index 0000000..9c91e4a --- /dev/null +++ b/aplcore/unit/command/unittest_command_log.cpp @@ -0,0 +1,143 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "../testeventloop.h" + +#include "apl/engine/event.h" +#include "apl/primitives/object.h" + +using namespace apl; + +class CommandLogTest : public CommandTest {}; + +static const char *LOG_WITH_ARGUMENTS = R"apl({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "onPress": [ + { + "type": "Log", + "level": "warn", + "message": "Small warning", + "arguments": [ + "A", + "B", + "${event.source.type}" + ] + } + ] + } + } +})apl"; + +TEST_F(CommandLogTest, LogWithArguments) +{ + loadDocument(LOG_WITH_ARGUMENTS); + ASSERT_TRUE(component); + + performClick(10, 10); + advanceTime(500); + + ASSERT_EQ(1, session->logCommandMessages.size()); + + auto m = session->logCommandMessages[0]; + ASSERT_EQ(LogLevel::kWarn, m.level); + ASSERT_EQ("Small warning", m.text); + + auto arguments = m.arguments.getArray(); + ASSERT_EQ(3, arguments.size()); + ASSERT_EQ("A", arguments.at(0).asString()); + ASSERT_EQ("B", arguments.at(1).asString()); + ASSERT_EQ("TouchWrapper", arguments.at(2).asString()); + + auto source = m.origin.getMap(); + ASSERT_EQ("TouchWrapper", source.at("type").asString()); +} + +static const char *LOG_WITH_LEVEL_VARIANTS = R"apl({ + "type": "APL", + "version": "2023.3", + "onMount": [ + { + "type": "Log" + }, + { + "type": "Log", + "level": "error", + "message": "Error as enum string" + }, + { + "type": "Log", + "level": "${Log.CRITICAL}", + "message": "Critical as constant" + }, + { + "type": "Log", + "level": "${Log.levelValue('warn')}", + "message": "Warn as value" + }, + { + "type": "Log", + "level": "${Log.levelName(Log.ERROR)}", + "message": "Error as name" + }, + { + "type": "Log", + "level": "whatever", + "message": "Unsupported level defaults to info" + }, + { + "type": "Log", + "level": 0, + "message": "Zero happens to be DEBUG" + } + ], + "mainTemplate": { + "items": { + "type": "Text", + "text": "Hello, logger!" + } + } +})apl"; + +TEST_F(CommandLogTest, LogSupportsNumericLevels) +{ + loadDocument(LOG_WITH_LEVEL_VARIANTS); + ASSERT_TRUE(component); + + const std::vector> expected = { + {LogLevel::kInfo, ""}, // Info and blank message by default + {LogLevel::kError, "Error as enum string"}, + {LogLevel::kCritical, "Critical as constant"}, + {LogLevel::kWarn, "Warn as value"}, + {LogLevel::kError, "Error as name"}, + {LogLevel::kInfo, "Unsupported level defaults to info"}, + {LogLevel::kDebug, "Zero happens to be DEBUG"}, + }; + + auto& actual = session->logCommandMessages; + ASSERT_EQ(expected.size(), actual.size()); + + for (int i = 0; i < expected.size(); i++) { + ASSERT_EQ(expected[i].first, actual[i].level); + ASSERT_EQ(expected[i].second, actual[i].text); + ASSERT_EQ(ObjectArray{}, actual[i].arguments.getArray()); + ASSERT_EQ("Document", actual[i].origin.getMap().at("type").asString()); + } +} diff --git a/aplcore/unit/command/unittest_command_page.cpp b/aplcore/unit/command/unittest_command_page.cpp index 24646b2..e39b114 100644 --- a/aplcore/unit/command/unittest_command_page.cpp +++ b/aplcore/unit/command/unittest_command_page.cpp @@ -32,6 +32,18 @@ class CommandPageTest : public CommandTest { return executeCommands(doc, false); } + ActionPtr executeSetPage(const std::string& component, const std::string& position, int value, int transitionDuration) { + rapidjson::Value cmd(rapidjson::kObjectType); + auto& alloc = doc.GetAllocator(); + cmd.AddMember("type", "SetPage", alloc); + cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); + cmd.AddMember("position", rapidjson::Value(position.c_str(), alloc).Move(), alloc); + cmd.AddMember("transitionDuration", transitionDuration, alloc); + cmd.AddMember("value", value, alloc); + doc.SetArray().PushBack(cmd, alloc); + return executeCommands(doc, false); + } + ActionPtr executeAutoPage(const std::string& component, int count, int duration) { rapidjson::Value cmd(rapidjson::kObjectType); auto& alloc = doc.GetAllocator(); @@ -43,6 +55,18 @@ class CommandPageTest : public CommandTest { return executeCommands(doc, false); } + ActionPtr executeAutoPage(const std::string& component, int count, int duration, int transitionDuration) { + rapidjson::Value cmd(rapidjson::kObjectType); + auto& alloc = doc.GetAllocator(); + cmd.AddMember("type", "AutoPage", alloc); + cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); + cmd.AddMember("count", count, alloc); + cmd.AddMember("duration", duration, alloc); + cmd.AddMember("transitionDuration", transitionDuration, alloc); + doc.SetArray().PushBack(cmd, alloc); + return executeCommands(doc, false); + } + ::testing::AssertionResult CheckChild(size_t idx, const std::string& id, const Rect& bounds) { auto child = component->getChildAt(idx); @@ -718,3 +742,184 @@ TEST_F(CommandPageTest, AutoPagerOnMountWithDelay) advanceTime(600); ASSERT_EQ(0, component->pagePosition()); } + +static const char *SIMPLER_PAGER = R"apl({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "Pager", + "id": "myPager", + "width": "100%", + "height": "100%", + "data": [ "red", "green", "blue", "orange" ], + "items": [ + { + "type": "Frame", + "backgroundColor": "${data}" + } + ] + } + } +})apl"; + +TEST_F(CommandPageTest, SetPageTransitionRelativeDuration) +{ + loadDocument(SIMPLER_PAGER); + ASSERT_EQ(0, component->pagePosition()); + + executeSetPage("myPager", "relative", 1, 300); + + advanceTime(300); + ASSERT_EQ(1, component->pagePosition()); +} + +TEST_F(CommandPageTest, AutoPageTransitionDuration) +{ + loadDocument(SIMPLER_PAGER); + ASSERT_EQ(0, component->pagePosition()); + + executeAutoPage("myPager", 2, 100, 300); + + advanceTime(300); + ASSERT_EQ(1, component->pagePosition()); + + advanceTime(100); + ASSERT_EQ(1, component->pagePosition()); + + advanceTime(300); + ASSERT_EQ(2, component->pagePosition()); +} + +TEST_F(CommandPageTest, SetPageTransitionDurationAbsolute) +{ + loadDocument(SIMPLER_PAGER); + ASSERT_EQ(0, component->pagePosition()); + + executeSetPage("myPager", "absolute", 2, 300); + + advanceTime(150); + ASSERT_EQ(0, component->pagePosition()); + advanceTime(150); + ASSERT_EQ(2, component->pagePosition()); +} + +TEST_F(CommandPageTest, SetPageTransitionZeroDurationAbsolute) +{ + loadDocument(SIMPLER_PAGER); + ASSERT_EQ(0, component->pagePosition()); + + executeSetPage("myPager", "absolute", 2, 0); + + advanceTime(1); + ASSERT_EQ(2, component->pagePosition()); +} + +TEST_F(CommandPageTest, SetPageTransitionZeroDurationRelative) +{ + loadDocument(SIMPLER_PAGER); + ASSERT_EQ(0, component->pagePosition()); + + executeSetPage("myPager", "relative", 2, 0); + + advanceTime(1); + ASSERT_EQ(2, component->pagePosition()); +} + +static const char *SIMPLER_PAGER_WITH_HANDLER = R"apl({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "Pager", + "id": "myPager", + "width": "100%", + "height": "100%", + "handlePageMove": [ + { + "drawOrder": "higherAbove", + "commands": [ + { + "type": "SetValue", + "componentId": "${event.nextChild.uid}", + "property": "transform", + "value": [ + { "translateX": "-${100 * event.amount}%" } + ] + } + ] + } + ], + "data": [ "red", "green", "blue", "orange" ], + "items": [ + { + "type": "Frame", + "backgroundColor": "${data}" + } + ] + } + } +})apl"; + +TEST_F(CommandPageTest, SetPageTransitionDurationRelativeWithHandler) +{ + loadDocument(SIMPLER_PAGER_WITH_HANDLER); + ASSERT_EQ(0, component->pagePosition()); + + executeSetPage("myPager", "relative", 1, 300); + + advanceTime(300); + ASSERT_EQ(1, component->pagePosition()); +} + +TEST_F(CommandPageTest, AutoPageTransitionDurationWithHandler) +{ + loadDocument(SIMPLER_PAGER_WITH_HANDLER); + ASSERT_EQ(0, component->pagePosition()); + + executeAutoPage("myPager", 2, 100, 300); + + advanceTime(300); + ASSERT_EQ(1, component->pagePosition()); + + advanceTime(100); + ASSERT_EQ(1, component->pagePosition()); + + advanceTime(300); + ASSERT_EQ(2, component->pagePosition()); +} + +TEST_F(CommandPageTest, SetPageTransitionDurationAbsoluteWithHandler) +{ + loadDocument(SIMPLER_PAGER_WITH_HANDLER); + ASSERT_EQ(0, component->pagePosition()); + + executeSetPage("myPager", "absolute", 2, 300); + + advanceTime(150); + ASSERT_EQ(0, component->pagePosition()); + advanceTime(150); + ASSERT_EQ(2, component->pagePosition()); +} + +TEST_F(CommandPageTest, SetPageTransitionZeroDurationAbsoluteWithHandler) +{ + loadDocument(SIMPLER_PAGER_WITH_HANDLER); + ASSERT_EQ(0, component->pagePosition()); + + executeSetPage("myPager", "absolute", 2, 0); + + advanceTime(1); + ASSERT_EQ(2, component->pagePosition()); +} + +TEST_F(CommandPageTest, SetPageTransitionZeroDurationRelativeWithHandler) +{ + loadDocument(SIMPLER_PAGER_WITH_HANDLER); + ASSERT_EQ(0, component->pagePosition()); + + executeSetPage("myPager", "relative", 2, 0); + + advanceTime(1); + ASSERT_EQ(2, component->pagePosition()); +} \ No newline at end of file diff --git a/aplcore/unit/command/unittest_commands.cpp b/aplcore/unit/command/unittest_commands.cpp index a2ccdba..6cfe00c 100644 --- a/aplcore/unit/command/unittest_commands.cpp +++ b/aplcore/unit/command/unittest_commands.cpp @@ -362,318 +362,6 @@ TEST_F(CommandTest, OnPressCommandArrayTerminate) ASSERT_EQ(0, loop->size()); } -const char * SEQ_TEST = R"({ - "type": "APL", - "version": "1.0", - "mainTemplate": { - "parameters": [ - "payload" - ], - "item": { - "type": "TouchWrapper", - "onPress": { - "type": "Sequential", - "delay": 100, - "repeatCount": 1, - "commands": { - "type": "SendEvent" - } - } - } - } -})"; - -TEST_F(CommandTest, SequentialTest) -{ - loadDocument(SEQ_TEST, DATA); - - auto map = component->getCalculated(); - auto onPress = map["onPress"]; - - performClick(1, 1); - - // The sequential command has been created; now we must wait for 100 - ASSERT_EQ(1, mCommandCount[kCommandTypeSequential]); - ASSERT_EQ(0, mCommandCount[kCommandTypeSendEvent]); - ASSERT_EQ(0, mActionCount[kCommandTypeSequential]); - ASSERT_EQ(0, mActionCount[kCommandTypeSendEvent]); - - loop->advanceToEnd(); // Each command should have fired appropriately - ASSERT_EQ(1, mCommandCount[kCommandTypeSequential]); - ASSERT_EQ(2, mCommandCount[kCommandTypeSendEvent]); - ASSERT_EQ(1, mActionCount[kCommandTypeSequential]); - ASSERT_EQ(2, mActionCount[kCommandTypeSendEvent]); - - ASSERT_TRUE(root->hasEvent()); - root->popEvent(); - ASSERT_TRUE(root->hasEvent()); - root->popEvent(); -} - -static const char *TRY_CATCH_FINALLY = R"({ - "type": "APL", - "version": "1.1", - "mainTemplate": { - "item": { - "type": "TouchWrapper", - "onPress": { - "type": "Sequential", - "repeatCount": 2, - "commands": { - "type": "Custom", - "delay": 1000, - "arguments": [ - "try" - ] - }, - "catch": [ - { - "type": "Custom", - "arguments": [ - "catch1" - ], - "delay": 1000 - }, - { - "type": "Custom", - "arguments": [ - "catch2" - ], - "delay": 1000 - }, - { - "type": "Custom", - "arguments": [ - "catch3" - ], - "delay": 1000 - } - ], - "finally": [ - { - "type": "Custom", - "arguments": [ - "finally1" - ], - "delay": 1000 - }, - { - "type": "Custom", - "arguments": [ - "finally2" - ], - "delay": 1000 - }, - { - "type": "Custom", - "arguments": [ - "finally3" - ], - "delay": 1000 - } - ] - } - } - } -})"; - - -// Let the entire command run normally through the "try" and "finally" parts -TEST_F(CommandTest, TryCatchFinally) -{ - loadDocument(TRY_CATCH_FINALLY); - performClick(1, 1); - - // Time 0 - ASSERT_FALSE(root->hasEvent()); - - // Standard commands - for (int i = 0 ; i < 3 ; i++) { - loop->advanceToTime(1000 + 1000 * i); - ASSERT_TRUE(root->hasEvent()); - auto event = root->popEvent(); - ASSERT_EQ(Object("try"), event.getValue(kEventPropertyArguments).at(0)); - ASSERT_FALSE(root->hasEvent()); - } - - // Finally commands, running in normal mode - for (int i = 0 ; i < 3 ; i++) { - loop->advanceToTime(4000 + 1000 * i); - ASSERT_TRUE(root->hasEvent()); - auto event = root->popEvent(); - std::string str = "finally"+std::to_string(i+1); - ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); - ASSERT_FALSE(root->hasEvent()); - } -} - -// Abort immediately. This should run only catch and finally commands -TEST_F(CommandTest, TryCatchFinallyAbortImmediately) -{ - loadDocument(TRY_CATCH_FINALLY); - performClick(1, 1); - - ASSERT_FALSE(root->hasEvent()); - root->cancelExecution(); // Cancel immediately. This should switch to fastmode catch commands and finally commands - - // Catch commands - for (int i = 0 ; i < 3 ; i++) { - ASSERT_TRUE(root->hasEvent()); - auto event = root->popEvent(); - std::string str = "catch"+std::to_string(i+1); - ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); - } - - // Finally commands, running in fast mode - for (int i = 0 ; i < 3 ; i++) { - ASSERT_TRUE(root->hasEvent()); - auto event = root->popEvent(); - std::string str = "finally"+std::to_string(i+1); - ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); - } - - ASSERT_FALSE(root->hasEvent()); -} - -// Abort after a few "try" commands have run. This should execute catch and finally. -TEST_F(CommandTest, TryCatchFinallyAbortAfterOne) -{ - loadDocument(TRY_CATCH_FINALLY); - performClick(1, 1); - - ASSERT_FALSE(root->hasEvent()); - - // Standard commands - loop->advanceToTime(1000); - ASSERT_TRUE(root->hasEvent()); - auto evt = root->popEvent(); - ASSERT_EQ(Object("try"), evt.getValue(kEventPropertyArguments).at(0)); - ASSERT_FALSE(root->hasEvent()); - - root->cancelExecution(); // Cancel. This should run catch commands - - // Catch commands - for (int i = 0 ; i < 3 ; i++) { - ASSERT_TRUE(root->hasEvent()); - auto event = root->popEvent(); - std::string str = "catch"+std::to_string(i+1); - ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); - } - - // Finally commands, running in fast mode - for (int i = 0 ; i < 3 ; i++) { - ASSERT_TRUE(root->hasEvent()); - auto event = root->popEvent(); - std::string str = "finally"+std::to_string(i+1); - ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); - } - - ASSERT_FALSE(root->hasEvent()); -} - -// Abort after all of the regular commands, but before finally commands start. -TEST_F(CommandTest, TryCatchFinallyAbortAfterTry) -{ - loadDocument(TRY_CATCH_FINALLY); - performClick(1, 1); - - ASSERT_FALSE(root->hasEvent()); - - // Standard commands - for (int i = 0 ; i < 3 ; i++) { - loop->advanceToTime(1000 + 1000 * i); - ASSERT_TRUE(root->hasEvent()); - auto event = root->popEvent(); - ASSERT_EQ(Object("try"), event.getValue(kEventPropertyArguments).at(0)); - ASSERT_FALSE(root->hasEvent()); - } - - root->cancelExecution(); - - // The first "finally" command was queued up and has been terminated, so we see the OTHER finally commands - for (int i = 1 ; i < 3 ; i++) { - ASSERT_TRUE(root->hasEvent()); - auto event = root->popEvent(); - std::string str = "finally"+std::to_string(i+1); - ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); - } - - ASSERT_FALSE(root->hasEvent()); -} - - - -const char *PARALLEL_TEST = R"({ - "type": "APL", - "version": "1.1", - "mainTemplate": { - "parameters": [ - "payload" - ], - "item": { - "type": "TouchWrapper", - "onPress": { - "type": "Parallel", - "commands": [ - { - "type": "Idle" - }, - { - "type": "Idle", - "when": false - }, - { - "type": "Idle", - "delay": 100 - }, - { - "type": "Idle", - "delay": 150, - "when": false - }, - { - "type": "Idle", - "delay": 200, - "when": true - } - ] - } - } - } -})"; - -TEST_F(CommandTest, ParallelTest) -{ - loadDocument(PARALLEL_TEST, DATA); - - auto map = component->getCalculated(); - auto onPress = map["onPress"]; - - performClick(1, 1); - - loop->advanceToEnd(); - ASSERT_EQ(3, mCommandCount[kCommandTypeIdle]); - ASSERT_EQ(3, mActionCount[kCommandTypeIdle]); - ASSERT_EQ(200, loop->currentTime()); -} - -TEST_F(CommandTest, ParallelTestTerminated) -{ - loadDocument(PARALLEL_TEST, DATA); - - auto map = component->getCalculated(); - auto onPress = map["onPress"]; - - performClick(1, 1); - - loop->advanceToTime(100); - context->sequencer().reset(); - - ASSERT_EQ(3, mCommandCount[kCommandTypeIdle]); - ASSERT_EQ(2, mActionCount[kCommandTypeIdle]); // One Idle doesn't fire until 200 - ASSERT_EQ(100, loop->currentTime()); -} - const char *LARGE_TEST = R"({ "type": "APL", "version": "1.1", @@ -812,51 +500,6 @@ TEST_F(CommandTest, ParallelSequentialMix) ASSERT_TRUE(IsEqual(Color(session, "yellow"), frame2->getCalculated(kPropertyBackgroundColor))); } - -static const char *REPEATED_SET_VALUE = R"({ - "type": "APL", - "version": "1.1", - "mainTemplate": { - "item": { - "type": "TouchWrapper", - "width": 100, - "height": 100, - "items": { - "type": "Text", - "text": "Woof", - "id": "dogText" - }, - "onPress": { - "type": "Sequential", - "repeatCount": 6, - "commands": { - "type": "SetValue", - "componentId": "dogText", - "property": "opacity", - "value": "${event.target.opacity - 0.2}", - "delay": 100 - } - } - } - } -})"; - -TEST_F(CommandTest, RepeatedSetValue) -{ - loadDocument(REPEATED_SET_VALUE); - auto text = component->getChildAt(0); - - performClick(1, 1); - - ASSERT_FALSE(root->hasEvent()); - - for (int i = 1 ; i <= 7 ; i++) { - loop->advanceToTime(i * 100 + 1); - ASSERT_NEAR(std::max(1.0 - i * 0.2, 0.0), - text->getCalculated(kPropertyOpacity).asNumber(), .001); - } -} - static const char *SET_STATE_DISABLED = R"({ "type": "APL", "version": "1.1", @@ -1510,3 +1153,131 @@ TEST_F(CommandTest, BindingUpdateTransform){ ASSERT_TRUE(IsEqual(Transform2D().translateX(500), text->getCalculated(kPropertyTransform).get())); } + +static const char *SIMPLE_VIDEO_DOCUMENT = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "items": { + "type": "Container", + "items": { + "type": "Video", + "id": "VIDEO" + } + } + } +})"; + +TEST_F(CommandTest, DisallowedDoesntExecuteControlMedia) { + config->set(RootProperty::kDisallowVideo, true); + + loadDocument(SIMPLE_VIDEO_DOCUMENT); + ASSERT_TRUE(component); + auto v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_TRUE(v->isDisallowed()); + + executeCommand("ControlMedia", {{"componentId", "VIDEO"}, {"command", "play"}}, false); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); + + executeCommand("ControlMedia", {{"componentId", "VIDEO"}, {"command", "pause"}}, false); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); + + executeCommand("ControlMedia", {{"componentId", "VIDEO"}, {"command", "next"}}, false); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); + + executeCommand("ControlMedia", {{"componentId", "VIDEO"}, {"command", "previous"}}, false); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); + + executeCommand("ControlMedia", {{"componentId", "VIDEO"}, {"command", "rewind"}}, false); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); + + executeCommand("ControlMedia", {{"componentId", "VIDEO"}, {"command", "seek"}, {"value", 900}}, false); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); + + executeCommand("ControlMedia", {{"componentId", "VIDEO"}, {"command", "seekTo"}, {"value", 900}}, false); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); + + executeCommand("ControlMedia", {{"componentId", "VIDEO"}, {"command", "setTrack"}, {"value", 2}}, false); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(CommandTest, DisallowedDoesntExecutePlayMedia) { + config->set(RootProperty::kDisallowVideo, true); + + loadDocument(SIMPLE_VIDEO_DOCUMENT); + ASSERT_TRUE(component); + + executeCommand("PlayMedia", {{"componentId", "VIDEO"}, {"source", "http://music.amazon.com/s3/MAGIC_TRACK_HERE"}, {"audioTrack", "foreground"}}, false); + + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(CommandTest, DisallowedDoesntExecuteSetValue) { + config->set(RootProperty::kDisallowVideo, true); + + loadDocument(SIMPLE_VIDEO_DOCUMENT); + ASSERT_TRUE(component); + + component->setProperty(apl::kPropertyOpacity, 0.5); + executeCommand("SetValue", {{"componentId", "VIDEO"}, {"property", "opacity"}, {"value", 1}}, false); + + ASSERT_EQ(0.5, component->getProperty(kPropertyOpacity).asFloat()); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(CommandTest, DisallowedDoesntExecuteSetState) { + config->set(RootProperty::kDisallowVideo, true); + + loadDocument(SIMPLE_VIDEO_DOCUMENT); + ASSERT_TRUE(component); + + component->setState(apl::kStateDisabled, true); + executeCommand("SetState", {{"componentId", "VIDEO"}, {"state", "disabled"}, {"value", false}}, false); + ASSERT_EQ(true, component->getState().get(kStateDisabled)); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); + + component->setState(apl::kStateChecked, true); + executeCommand("SetState", {{"componentId", "VIDEO"}, {"state", "checked"}, {"value", false}}, false); + ASSERT_EQ(true, component->getState().get(kStateChecked)); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(CommandTest, DisallowedDoesntExecuteSetFocus) { + config->set(RootProperty::kDisallowVideo, true); + + loadDocument(SIMPLE_VIDEO_DOCUMENT); + ASSERT_TRUE(component); + + component->setState(apl::kStateFocused, false); + executeCommand("SetFocus", {{"componentId", "VIDEO"}}, false); + + ASSERT_EQ(false, component->getState().get(kStateFocused)); + ASSERT_FALSE(root->hasEvent()); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(CommandTest, DisallowedDoesntExecuteClearFocus) { + config->set(RootProperty::kDisallowVideo, true); + + loadDocument(SIMPLE_VIDEO_DOCUMENT); + ASSERT_TRUE(component); + + component->setState(apl::kStateFocused, true); + executeCommand("ClearFocus", {}, false); + + ASSERT_EQ(true, component->getState().get(kStateFocused)); + ASSERT_FALSE(root->hasEvent()); + ASSERT_FALSE(ConsoleMessage()); +} \ No newline at end of file diff --git a/aplcore/unit/command/unittest_commands_parallel.cpp b/aplcore/unit/command/unittest_commands_parallel.cpp new file mode 100644 index 0000000..70eeb23 --- /dev/null +++ b/aplcore/unit/command/unittest_commands_parallel.cpp @@ -0,0 +1,195 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include + +#include "apl/time/sequencer.h" +#include "apl/engine/event.h" + +#include "../testeventloop.h" + +using namespace apl; + +const char *PARALLEL_TEST = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "Parallel", + "commands": [ + { + "type": "Idle" + }, + { + "type": "Idle", + "when": false + }, + { + "type": "Idle", + "delay": 100 + }, + { + "type": "Idle", + "delay": 150, + "when": false + }, + { + "type": "Idle", + "delay": 200, + "when": true + } + ] + } + } + } +})"; + +TEST_F(CommandTest, ParallelTest) +{ + loadDocument(PARALLEL_TEST, R"({ "title": "Pecan Pie V" })"); + + auto map = component->getCalculated(); + auto onPress = map["onPress"]; + + performClick(1, 1); + + loop->advanceToEnd(); + ASSERT_EQ(3, mCommandCount[kCommandTypeIdle]); + ASSERT_EQ(3, mActionCount[kCommandTypeIdle]); + ASSERT_EQ(200, loop->currentTime()); +} + +TEST_F(CommandTest, ParallelTestTerminated) +{ + loadDocument(PARALLEL_TEST, R"({ "title": "Pecan Pie V" })"); + + auto map = component->getCalculated(); + auto onPress = map["onPress"]; + + performClick(1, 1); + + loop->advanceToTime(100); + context->sequencer().reset(); + + ASSERT_EQ(3, mCommandCount[kCommandTypeIdle]); + ASSERT_EQ(2, mActionCount[kCommandTypeIdle]); // One Idle doesn't fire until 200 + ASSERT_EQ(100, loop->currentTime()); +} + +const char *PARALLEL_DATA_TEST = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "Parallel", + "data": [ + { "delay": 250, "argument": "first" }, + { "delay": 300, "argument": "second" }, + { "delay": 350, "argument": "third" } + ], + "commands": [ + { + "delay": "${data.delay}", + "type": "SendEvent", + "arguments": [ "first", "${data.argument}" ] + }, + { + "delay": "${data.delay}", + "type": "SendEvent", + "arguments": [ "second", "${data.argument}" ] + }, + { + "delay": "${data.delay}", + "type": "SendEvent", + "arguments": [ "third", "${data.argument}" ] + } + ] + } + } + } +})"; + +TEST_F(CommandTest, ParallelDataTest) +{ + loadDocument(PARALLEL_DATA_TEST); + + auto map = component->getCalculated(); + + performClick(1, 1); + + // We create sequence of commands for every data element and execute them in parallel. + + // First data sequence, 250 ms + advanceTime(250); + + ASSERT_TRUE(CheckSendEvent(root, "first", "first")); + ASSERT_TRUE(!root->hasEvent()); + + // Second data sequence, 300 ms + advanceTime(50); + + ASSERT_TRUE(CheckSendEvent(root, "first", "second")); + ASSERT_TRUE(!root->hasEvent()); + + // Third data sequence, 350 ms + advanceTime(50); + + ASSERT_TRUE(CheckSendEvent(root, "first", "third")); + ASSERT_TRUE(!root->hasEvent()); + + // First data sequence 500 ms + advanceTime(150); + + ASSERT_TRUE(CheckSendEvent(root, "second", "first")); + ASSERT_TRUE(!root->hasEvent()); + + // Second data sequence 600 ms + advanceTime(100); + + ASSERT_TRUE(CheckSendEvent(root, "second", "second")); + ASSERT_TRUE(!root->hasEvent()); + + // Third data sequence 700 ms + advanceTime(100); + + ASSERT_TRUE(CheckSendEvent(root, "second", "third")); + ASSERT_TRUE(!root->hasEvent()); + + + // First data sequence 750 ms + advanceTime(50); + + ASSERT_TRUE(CheckSendEvent(root, "third", "first")); + ASSERT_TRUE(!root->hasEvent()); + + // Second data sequence 900 ms + advanceTime(150); + + ASSERT_TRUE(CheckSendEvent(root, "third", "second")); + ASSERT_TRUE(!root->hasEvent()); + + // Third data sequence 1050 ms + advanceTime(150); + + ASSERT_TRUE(CheckSendEvent(root, "third", "third")); + ASSERT_TRUE(!root->hasEvent()); +} diff --git a/aplcore/unit/command/unittest_commands_sequential.cpp b/aplcore/unit/command/unittest_commands_sequential.cpp new file mode 100644 index 0000000..33c11cb --- /dev/null +++ b/aplcore/unit/command/unittest_commands_sequential.cpp @@ -0,0 +1,401 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include + +#include "apl/time/sequencer.h" +#include "apl/engine/event.h" + +#include "../testeventloop.h" + +using namespace apl; + + +const char * SEQ_TEST = R"({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "Sequential", + "delay": 100, + "repeatCount": 1, + "commands": { + "type": "SendEvent" + } + } + } + } +})"; + +TEST_F(CommandTest, SequentialTest) +{ + loadDocument(SEQ_TEST, R"({ "title": "Pecan Pie V" })"); + + auto map = component->getCalculated(); + auto onPress = map["onPress"]; + + performClick(1, 1); + + // The sequential command has been created; now we must wait for 100 + ASSERT_EQ(1, mCommandCount[kCommandTypeSequential]); + ASSERT_EQ(0, mCommandCount[kCommandTypeSendEvent]); + ASSERT_EQ(0, mActionCount[kCommandTypeSequential]); + ASSERT_EQ(0, mActionCount[kCommandTypeSendEvent]); + + loop->advanceToEnd(); // Each command should have fired appropriately + ASSERT_EQ(1, mCommandCount[kCommandTypeSequential]); + ASSERT_EQ(2, mCommandCount[kCommandTypeSendEvent]); + ASSERT_EQ(1, mActionCount[kCommandTypeSequential]); + ASSERT_EQ(2, mActionCount[kCommandTypeSendEvent]); + + ASSERT_TRUE(root->hasEvent()); + root->popEvent(); + ASSERT_TRUE(root->hasEvent()); + root->popEvent(); +} + +static const char *TRY_CATCH_FINALLY = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "Sequential", + "repeatCount": 2, + "commands": { + "type": "Custom", + "delay": 1000, + "arguments": [ + "try" + ] + }, + "catch": [ + { + "type": "Custom", + "arguments": [ + "catch1" + ], + "delay": 1000 + }, + { + "type": "Custom", + "arguments": [ + "catch2" + ], + "delay": 1000 + }, + { + "type": "Custom", + "arguments": [ + "catch3" + ], + "delay": 1000 + } + ], + "finally": [ + { + "type": "Custom", + "arguments": [ + "finally1" + ], + "delay": 1000 + }, + { + "type": "Custom", + "arguments": [ + "finally2" + ], + "delay": 1000 + }, + { + "type": "Custom", + "arguments": [ + "finally3" + ], + "delay": 1000 + } + ] + } + } + } +})"; + + +// Let the entire command run normally through the "try" and "finally" parts +TEST_F(CommandTest, TryCatchFinally) +{ + loadDocument(TRY_CATCH_FINALLY); + performClick(1, 1); + + // Time 0 + ASSERT_FALSE(root->hasEvent()); + + // Standard commands + for (int i = 0 ; i < 3 ; i++) { + loop->advanceToTime(1000 + 1000 * i); + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(Object("try"), event.getValue(kEventPropertyArguments).at(0)); + ASSERT_FALSE(root->hasEvent()); + } + + // Finally commands, running in normal mode + for (int i = 0 ; i < 3 ; i++) { + loop->advanceToTime(4000 + 1000 * i); + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + std::string str = "finally"+std::to_string(i+1); + ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); + ASSERT_FALSE(root->hasEvent()); + } +} + +// Abort immediately. This should run only catch and finally commands +TEST_F(CommandTest, TryCatchFinallyAbortImmediately) +{ + loadDocument(TRY_CATCH_FINALLY); + performClick(1, 1); + + ASSERT_FALSE(root->hasEvent()); + root->cancelExecution(); // Cancel immediately. This should switch to fastmode catch commands and finally commands + + // Catch commands + for (int i = 0 ; i < 3 ; i++) { + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + std::string str = "catch"+std::to_string(i+1); + ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); + } + + // Finally commands, running in fast mode + for (int i = 0 ; i < 3 ; i++) { + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + std::string str = "finally"+std::to_string(i+1); + ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); + } + + ASSERT_FALSE(root->hasEvent()); +} + +// Abort after a few "try" commands have run. This should execute catch and finally. +TEST_F(CommandTest, TryCatchFinallyAbortAfterOne) +{ + loadDocument(TRY_CATCH_FINALLY); + performClick(1, 1); + + ASSERT_FALSE(root->hasEvent()); + + // Standard commands + loop->advanceToTime(1000); + ASSERT_TRUE(root->hasEvent()); + auto evt = root->popEvent(); + ASSERT_EQ(Object("try"), evt.getValue(kEventPropertyArguments).at(0)); + ASSERT_FALSE(root->hasEvent()); + + root->cancelExecution(); // Cancel. This should run catch commands + + // Catch commands + for (int i = 0 ; i < 3 ; i++) { + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + std::string str = "catch"+std::to_string(i+1); + ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); + } + + // Finally commands, running in fast mode + for (int i = 0 ; i < 3 ; i++) { + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + std::string str = "finally"+std::to_string(i+1); + ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); + } + + ASSERT_FALSE(root->hasEvent()); +} + +// Abort after all of the regular commands, but before finally commands start. +TEST_F(CommandTest, TryCatchFinallyAbortAfterTry) +{ + loadDocument(TRY_CATCH_FINALLY); + performClick(1, 1); + + ASSERT_FALSE(root->hasEvent()); + + // Standard commands + for (int i = 0 ; i < 3 ; i++) { + loop->advanceToTime(1000 + 1000 * i); + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(Object("try"), event.getValue(kEventPropertyArguments).at(0)); + ASSERT_FALSE(root->hasEvent()); + } + + root->cancelExecution(); + + // The first "finally" command was queued up and has been terminated, so we see the OTHER finally commands + for (int i = 1 ; i < 3 ; i++) { + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + std::string str = "finally"+std::to_string(i+1); + ASSERT_EQ(Object(str), event.getValue(kEventPropertyArguments).at(0)); + } + + ASSERT_FALSE(root->hasEvent()); +} + +static const char *REPEATED_SET_VALUE = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": 100, + "height": 100, + "items": { + "type": "Text", + "text": "Woof", + "id": "dogText" + }, + "onPress": { + "type": "Sequential", + "repeatCount": 6, + "commands": { + "type": "SetValue", + "componentId": "dogText", + "property": "opacity", + "value": "${event.target.opacity - 0.2}", + "delay": 100 + } + } + } + } +})"; + +TEST_F(CommandTest, RepeatedSetValue) +{ + loadDocument(REPEATED_SET_VALUE); + auto text = component->getChildAt(0); + + performClick(1, 1); + + ASSERT_FALSE(root->hasEvent()); + + for (int i = 1 ; i <= 7 ; i++) { + loop->advanceToTime(i * 100 + 1); + ASSERT_NEAR(std::max(1.0 - i * 0.2, 0.0), + text->getCalculated(kPropertyOpacity).asNumber(), .001); + } +} + +const char *SEQUENTIAL_DATA_TEST = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "Sequential", + "data": [ + { "delay": 250, "argument": "first" }, + { "delay": 300, "argument": "second" }, + { "delay": 350, "argument": "third" } + ], + "commands": [ + { + "delay": "${data.delay}", + "type": "SendEvent", + "arguments": [ "first", "${data.argument}" ] + }, + { + "delay": "${data.delay}", + "type": "SendEvent", + "arguments": [ "second", "${data.argument}" ] + }, + { + "delay": "${data.delay}", + "type": "SendEvent", + "arguments": [ "third", "${data.argument}" ] + } + ] + } + } + } +})"; + +TEST_F(CommandTest, SequentialDataTest) +{ + loadDocument(SEQUENTIAL_DATA_TEST); + + auto map = component->getCalculated(); + + performClick(1, 1); + + // We create sequence of commands for every data element and execute them in parallel. + + // First data sequence + advanceTime(250); + + ASSERT_TRUE(CheckSendEvent(root, "first", "first")); + ASSERT_TRUE(!root->hasEvent()); + + advanceTime(250); + + ASSERT_TRUE(CheckSendEvent(root, "second", "first")); + ASSERT_TRUE(!root->hasEvent()); + + advanceTime(250); + + ASSERT_TRUE(CheckSendEvent(root, "third", "first")); + ASSERT_TRUE(!root->hasEvent()); + + // Second data sequence + advanceTime(300); + + ASSERT_TRUE(CheckSendEvent(root, "first", "second")); + ASSERT_TRUE(!root->hasEvent()); + + advanceTime(300); + + ASSERT_TRUE(CheckSendEvent(root, "second", "second")); + ASSERT_TRUE(!root->hasEvent()); + + advanceTime(300); + + ASSERT_TRUE(CheckSendEvent(root, "third", "second")); + ASSERT_TRUE(!root->hasEvent()); + + // Third data sequence + advanceTime(350); + + ASSERT_TRUE(CheckSendEvent(root, "first", "third")); + ASSERT_TRUE(!root->hasEvent()); + + advanceTime(350); + + ASSERT_TRUE(CheckSendEvent(root, "second", "third")); + ASSERT_TRUE(!root->hasEvent()); + + advanceTime(350); + + ASSERT_TRUE(CheckSendEvent(root, "third", "third")); + ASSERT_TRUE(!root->hasEvent()); +} diff --git a/aplcore/unit/component/CMakeLists.txt b/aplcore/unit/component/CMakeLists.txt index c152087..93a4892 100644 --- a/aplcore/unit/component/CMakeLists.txt +++ b/aplcore/unit/component/CMakeLists.txt @@ -40,6 +40,7 @@ target_sources_local(unittest unittest_text_component.cpp unittest_tick.cpp unittest_transform.cpp + unittest_video_component.cpp unittest_visual_context.cpp unittest_visual_hash.cpp ) \ No newline at end of file diff --git a/aplcore/unit/component/unittest_accessibility_actions.cpp b/aplcore/unit/component/unittest_accessibility_actions.cpp index 019e3c8..4e2e12a 100644 --- a/aplcore/unit/component/unittest_accessibility_actions.cpp +++ b/aplcore/unit/component/unittest_accessibility_actions.cpp @@ -15,6 +15,8 @@ #include "../testeventloop.h" +#include "apl/focus/focusmanager.h" + using namespace apl; class AccessibilityActionTest : public DocumentWrapper {}; @@ -60,7 +62,7 @@ TEST_F(AccessibilityActionTest, Basic) // Invoke the action and verify that it changes the background color component->update(kUpdateAccessibilityAction, "MakeRed"); - ASSERT_TRUE(CheckDirty(component, kPropertyBackgroundColor, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(component, kPropertyBackgroundColor, kPropertyBackground, kPropertyVisualHash)); ASSERT_TRUE(CheckDirty(root, component)); ASSERT_TRUE(IsEqual(Color(Color::RED), component->getCalculated(kPropertyBackgroundColor))); @@ -372,6 +374,196 @@ TEST_F(AccessibilityActionTest, Gestures) ASSERT_TRUE(IsEqual("Tap", text->getCalculated(kPropertyText).asString())); } +static const char *PAGER_SCROLLING_TEST = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Pager", + "height": "100%", + "navigation": "wrap", + "items": { + "type": "Text", + "text": "${data}" + }, + "data": ["one", "two", "three"] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, PagerScrolling) +{ + loadDocument(PAGER_SCROLLING_TEST); + ASSERT_TRUE(component); + auto text = component->getChildAt(component->pagePosition()); + ASSERT_TRUE(text); + ASSERT_TRUE(IsEqual("one", text->getCalculated(kPropertyText).asString())); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + + text = component->getChildAt(component->pagePosition()); + ASSERT_TRUE(IsEqual("two", text->getCalculated(kPropertyText).asString())); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + + text = component->getChildAt(component->pagePosition()); + ASSERT_TRUE(IsEqual("one", text->getCalculated(kPropertyText).asString())); + + component->update(kUpdateAccessibilityAction, "scrollbackward"); + root->clearPending(); + + text = component->getChildAt(component->pagePosition()); + ASSERT_TRUE(IsEqual("three", text->getCalculated(kPropertyText).asString())); +} + +static const char *PAGER_SCROLLING_EXPLICIT = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Pager", + "height": "100%", + "navigation": "wrap", + "items": { + "type": "Text", + "text": "${data}" + }, + "data": ["one", "two", "three"], + "actions": [ + { + "name": "scrollforward", + "label": "scrollforward Test", + "enabled": false + }, + { + "name": "scrollbackward", + "label": "scrollbackward Test", + "commands": { + "type": "SendEvent", + "arguments": [ "scrollbackward" ] + } + } + ] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, PagerScrollingExplicit) +{ + loadDocument(PAGER_SCROLLING_EXPLICIT); + ASSERT_TRUE(component); + auto text = component->getChildAt(component->pagePosition()); + ASSERT_TRUE(text); + ASSERT_TRUE(IsEqual("one", text->getCalculated(kPropertyText).asString())); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + + text = component->getChildAt(component->pagePosition()); + ASSERT_TRUE(IsEqual("one", text->getCalculated(kPropertyText).asString())); + + component->update(kUpdateAccessibilityAction, "scrollbackward"); + root->clearPending(); + + text = component->getChildAt(component->pagePosition()); + ASSERT_TRUE(IsEqual("one", text->getCalculated(kPropertyText).asString())); + ASSERT_TRUE(CheckSendEvent(root, "scrollbackward")); +} + +static const char *SEQUENCE_SCROLLING_TEST = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Sequence", + "height": 100, + "items": { + "type": "Text", + "height": 100, + "text": "${data}" + }, + "data": ["one", "two", "three", "four"] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, SequenceScrolling) +{ + loadDocument(SEQUENCE_SCROLLING_TEST); + ASSERT_TRUE(component); + ASSERT_EQ(0, component->scrollPosition().getY()); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + + ASSERT_EQ(100, component->scrollPosition().getY()); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + + ASSERT_EQ(300, component->scrollPosition().getY()); + + component->update(kUpdateAccessibilityAction, "scrollbackward"); + root->clearPending(); + + ASSERT_EQ(200, component->scrollPosition().getY()); +} + +static const char *SEQUENCE_SCROLLING_EXPLICIT = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Pager", + "height": "100%", + "navigation": "wrap", + "items": { + "type": "Text", + "text": "${data}" + }, + "data": ["one", "two", "three"], + "actions": [ + { + "name": "scrollforward", + "label": "scrollforward Test", + "enabled": false + }, + { + "name": "scrollbackward", + "label": "scrollbackward Test", + "commands": { + "type": "SendEvent", + "arguments": [ "scrollbackward" ] + } + } + ] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, SequenceScrollingExplicit) +{ + loadDocument(SEQUENCE_SCROLLING_EXPLICIT); + ASSERT_TRUE(component); + ASSERT_EQ(0, component->scrollPosition().getY()); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + + ASSERT_EQ(0, component->scrollPosition().getY()); + + component->update(kUpdateAccessibilityAction, "scrollbackward"); + root->clearPending(); + + ASSERT_TRUE(CheckSendEvent(root, "scrollbackward")); +} + static const char *ACTIVATE_PREFERS_ON_PRESS_OVER_TAP_TEST = R"apl( { "type": "APL", @@ -396,10 +588,16 @@ static const char *ACTIVATE_PREFERS_ON_PRESS_OVER_TAP_TEST = R"apl( } } ], - "actions": { - "name": "activate", - "label": "Activate Test" - } + "actions": [ + { + "name": "activate", + "label": "Activate Test" + }, + { + "name": "tap", + "label": "Tap Test" + } + ] } } } @@ -420,6 +618,17 @@ TEST_F(AccessibilityActionTest, ActivatePrefersOnPressOverOnTap) ASSERT_FALSE(root->hasEvent()); } +TEST_F(AccessibilityActionTest, TapIsSeparateAction) +{ + loadDocument(ACTIVATE_PREFERS_ON_PRESS_OVER_TAP_TEST); + ASSERT_TRUE(component); + + component->update(kUpdateAccessibilityAction, "tap"); + root->clearPending(); + ASSERT_TRUE(CheckSendEvent(root, "onTap")); + ASSERT_FALSE(root->hasEvent()); +} + static const char *ACTIONS_WITH_COMMANDS = R"apl( { "type": "APL", @@ -800,3 +1009,952 @@ TEST_F(AccessibilityActionTest, ArgumentPassing) ASSERT_TRUE(CheckSendEvent(root, "Another Command Argument", "testAction2")); } +static const char *TOUCHABLE_DYNAMIC_ACTIONS = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Container", + "height": "100%", + "navigation": "normal", + "bind": [ + { "name": "ActionToggler", "type": "boolean", "value": false } + ], + "items": [ + { + "type": "TouchWrapper", + "actions": [{ "name": "activate", "label": "Activate with no onPress" }] + }, + { + "type": "TouchWrapper", + "actions": [{ "name": "activate", "label": "Activate with onPress" }], + "onPress": { "type": "SendEvent" } + }, + { + "type": "TouchWrapper", + "actions": [{ "name": "activate", "label": "Activate with Tap" }], + "gestures": { "type": "Tap", "onTap": { "type": "SendEvent" }} + }, + { + "type": "TouchWrapper", + "actions": [{ "name": "activate", "label": "Activate with onPress, disabled component" }], + "onPress": { "type": "SendEvent" }, + "disabled": true + }, + { + "type": "TouchWrapper", + "actions": [{ "name": "activate", "label": "Activate with disabled action", "enabled": "${ActionToggler}" }], + "onPress": { "type": "SendEvent" } + }, + { + "type": "TouchWrapper", + "actions": [ + { + "name": "activate", + "label": "Activate action with commands", + "commands": { "type": "SendEvent" } + } + ] + } + ] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, TouchableDynamicActionsOld) +{ + loadDocument(TOUCHABLE_DYNAMIC_ACTIONS); + ASSERT_TRUE(component); + + // In old "style" actions always reported if explicitly requested + ASSERT_EQ(1, component->getChildAt(0)->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(1, component->getChildAt(1)->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(1, component->getChildAt(2)->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(1, component->getChildAt(3)->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(1, component->getChildAt(4)->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(1, component->getChildAt(5)->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +TEST_F(AccessibilityActionTest, TouchableDynamicActions) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(TOUCHABLE_DYNAMIC_ACTIONS); + ASSERT_TRUE(component); + + // No onPress/commands or onTap available + ASSERT_EQ(0, component->getChildAt(0)->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Reported from onPress + ASSERT_EQ(1, component->getChildAt(1)->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Reported from Tap + ASSERT_EQ(1, component->getChildAt(2)->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Disabled component + ASSERT_EQ(0, component->getChildAt(3)->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Disabled action + ASSERT_EQ(0, component->getChildAt(4)->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Explicit command + ASSERT_EQ(1, component->getChildAt(5)->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Enabling disabled component should refresh actions + component->getCoreChildAt(3)->setProperty(apl::kPropertyDisabled, false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component->getCoreChildAt(3), kPropertyAccessibilityActions, kPropertyDisabled)); + ASSERT_EQ(1, component->getChildAt(3)->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Disabling enabled component should refresh actions too + component->getCoreChildAt(3)->setProperty(apl::kPropertyDisabled, true); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component->getCoreChildAt(3), kPropertyAccessibilityActions, kPropertyDisabled)); + ASSERT_EQ(0, component->getChildAt(3)->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Changing bound "enabled" in the action enables it + component->setProperty("ActionToggler", true); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component->getCoreChildAt(4), kPropertyAccessibilityActions)); + ASSERT_EQ(1, component->getChildAt(4)->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Changing bound "enabled" in the action also can disable it + component->setProperty("ActionToggler", false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component->getCoreChildAt(4), kPropertyAccessibilityActions)); + ASSERT_EQ(0, component->getChildAt(4)->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +static const char *TOUCHABLE_DYNAMIC_GESTURES = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "TouchWrapper", + "actions": [ + { "name": "tap", "label": "Enable Tap gesture accessibility" }, + { "name": "doubletap", "label": "Enable DoubleTap gesture accessibility" }, + { "name": "longpress", "label": "Enable LongPress gesture accessibility" }, + { "name": "swipeaway", "label": "Enable SwipeAway gesture accessibility" } + ], + "gestures": [ + { "type": "DoublePress", "onDoublePress": { "type": "SendEvent" } }, + { "type": "LongPress", "onLongPressEnd": { "type": "SendEvent" } }, + { "type": "SwipeAway", "direction": "left", "onSwipeDone": { "type": "SendEvent" } }, + { "type": "Tap", "onTap": { "type": "SendEvent" } } + ] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, TouchableDynamicGesturesOld) +{ + loadDocument(TOUCHABLE_DYNAMIC_GESTURES); + ASSERT_TRUE(component); + + ASSERT_EQ(4, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +TEST_F(AccessibilityActionTest, TouchableDynamicGestures) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(TOUCHABLE_DYNAMIC_GESTURES); + ASSERT_TRUE(component); + + ASSERT_EQ(4, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +static const char *TOUCHABLE_DYNAMIC_GESTURES_DISABLED = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "TouchWrapper", + "actions": [ + { "name": "tap", "label": "Enable Tap gesture accessibility", "enabled": false }, + { "name": "doubletap", "label": "Enable DoubleTap gesture accessibility", "enabled": false }, + { "name": "longpress", "label": "Enable LongPress gesture accessibility", "enabled": false }, + { "name": "swipeaway", "label": "Enable SwipeAway gesture accessibility", "enabled": false } + ], + "gestures": [ + { "type": "DoublePress", "onDoublePress": { "type": "SendEvent" } }, + { "type": "LongPress", "onLongPressEnd": { "type": "SendEvent" } }, + { "type": "SwipeAway", "direction": "left", "onSwipeDone": { "type": "SendEvent" } }, + { "type": "Tap", "onTap": { "type": "SendEvent" } } + ] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, TouchableDynamicGesturesDisabledOld) +{ + loadDocument(TOUCHABLE_DYNAMIC_GESTURES_DISABLED); + ASSERT_TRUE(component); + + ASSERT_EQ(4, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +TEST_F(AccessibilityActionTest, TouchableDynamicGesturesDisabled) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(TOUCHABLE_DYNAMIC_GESTURES_DISABLED); + ASSERT_TRUE(component); + + ASSERT_EQ(0, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +static const char *PAGER_DYNAMIC_ACTIONS = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Pager", + "height": "100%", + "navigation": "normal", + "items": { + "type": "TouchWrapper", + "actions": [ { "name": "activate", "label": "Activate" } ], + "onPress": { "type": "SendEvent" } + }, + "data": ["one", "two"], + "actions": [ + { + "name": "scrollbackward", + "label": "scrollbackward disabled Test", + "enabled": false + } + ] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, PagerDynamicActions) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(PAGER_DYNAMIC_ACTIONS); + ASSERT_TRUE(component); + + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + auto laidOutChild = component->getChildAt(component->pagePosition()); + ASSERT_TRUE(laidOutChild->getCalculated(apl::kPropertyLaidOut).asBoolean()); + ASSERT_TRUE(laidOutChild); + ASSERT_EQ(1, laidOutChild->getCalculated(apl::kPropertyAccessibilityActions).size()); + + auto nonLaidOutChild = component->getChildAt(1); + ASSERT_FALSE(nonLaidOutChild->getCalculated(apl::kPropertyLaidOut).asBoolean()); + ASSERT_TRUE(nonLaidOutChild); + ASSERT_EQ(0, nonLaidOutChild->getCalculated(apl::kPropertyAccessibilityActions).size()); + + // Switch page, newly laid-out components gets action published + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(nonLaidOutChild, + kPropertyLaidOut, kPropertyAccessibilityActions, kPropertyBounds, + kPropertyInnerBounds, kPropertyVisualHash, + kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(nonLaidOutChild->getCalculated(apl::kPropertyLaidOut).asBoolean()); + ASSERT_TRUE(nonLaidOutChild); + ASSERT_EQ(1, nonLaidOutChild->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +static const char *PAGER_DYNAMIC_SIMPLE_ACTIONS = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Pager", + "height": "100%", + "navigation": "normal", + "items": { + "type": "TouchWrapper" + }, + "data": ["one", "two", "three"] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, PagerDynamicSimpleActions) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(PAGER_DYNAMIC_SIMPLE_ACTIONS); + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(0, component->pagePosition()); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + ASSERT_EQ(1, component->pagePosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyCurrentPage, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + ASSERT_EQ(2, component->pagePosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyCurrentPage, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + component->update(kUpdateAccessibilityAction, "scrollbackward"); + root->clearPending(); + ASSERT_EQ(1, component->pagePosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyCurrentPage, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +TEST_F(AccessibilityActionTest, PagerDynamicSimpleActionsFromCommands) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(PAGER_DYNAMIC_SIMPLE_ACTIONS); + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(0, component->pagePosition()); + + rapidjson::Document scrollForwards; + scrollForwards.Parse(R"([{"type": "SetPage", "componentId": ":root", "position": "relative", "value": 1}])"); + rootDocument->executeCommands(scrollForwards, false); + advanceTime(1000); + root->clearPending(); + ASSERT_EQ(1, component->pagePosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyCurrentPage, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + rootDocument->executeCommands(scrollForwards, false); + advanceTime(1000); + root->clearPending(); + ASSERT_EQ(2, component->pagePosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyCurrentPage, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + rapidjson::Document scrollBackwards; + scrollBackwards.Parse(R"([{"type": "SetPage", "componentId": ":root", "position": "relative", "value": -1}])"); + rootDocument->executeCommands(scrollBackwards, false); + advanceTime(1000); + root->clearPending(); + ASSERT_EQ(1, component->pagePosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyCurrentPage, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +static const char *SEQUENCE_DYNAMIC_SIMPLE_ACTIONS = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Sequence", + "height": 100, + "items": { + "type": "TouchWrapper", + "height": "100%" + }, + "data": ["one", "two", "three"] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, SequenceDynamicSimpleActions) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(SEQUENCE_DYNAMIC_SIMPLE_ACTIONS); + + advanceTime(10); + + root->clearDirty(); + + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(Point(0, 0), component->scrollPosition()); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + ASSERT_EQ(Point(0, 200), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + component->update(kUpdateAccessibilityAction, "scrollbackward"); + root->clearPending(); + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +TEST_F(AccessibilityActionTest, SequenceDynamicSimpleActionsFromCommands) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(SEQUENCE_DYNAMIC_SIMPLE_ACTIONS); + + advanceTime(10); + + root->clearDirty(); + + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(Point(0, 0), component->scrollPosition()); + + rapidjson::Document scrollForwards; + scrollForwards.Parse(R"([{"type": "Scroll", "componentId": ":root", "distance": 1}])"); + rootDocument->executeCommands(scrollForwards, false); + advanceTime(1000); + root->clearPending(); + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + rootDocument->executeCommands(scrollForwards, false); + advanceTime(1000); + root->clearPending(); + ASSERT_EQ(Point(0, 200), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + rapidjson::Document scrollBackwards; + scrollBackwards.Parse(R"([{"type": "Scroll", "componentId": ":root", "distance": -1}])"); + rootDocument->executeCommands(scrollBackwards, false); + advanceTime(1000); + root->clearPending(); + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +static const char *SCROLLVIEW_DYNAMIC_SIMPLE_ACTIONS = R"apl({ +"type": "APL", +"version": "2023.2", +"mainTemplate": { + "items": { + "type": "ScrollView", + "height": 100, + "item": { + "type": "Container", + "height": 300, + "items": { + "type": "Frame", + "height": 100, + "backgroundColor": "${data}" + }, + "data": [ + "blue", + "green", + "red" + ] + } + } +} +})apl"; + +TEST_F(AccessibilityActionTest, ScrollviewDynamicSimpleActions) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(SCROLLVIEW_DYNAMIC_SIMPLE_ACTIONS); + + root->clearDirty(); + + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(Point(0, 0), component->scrollPosition()); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + component->update(kUpdateAccessibilityAction, "scrollforward"); + root->clearPending(); + ASSERT_EQ(Point(0, 200), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + component->update(kUpdateAccessibilityAction, "scrollbackward"); + root->clearPending(); + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +TEST_F(AccessibilityActionTest, ScrollviewDynamicSimpleActionsFromCommands) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(SCROLLVIEW_DYNAMIC_SIMPLE_ACTIONS); + + root->clearDirty(); + + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(Point(0, 0), component->scrollPosition()); + + rapidjson::Document scrollForwards; + scrollForwards.Parse(R"([{"type": "Scroll", "componentId": ":root", "distance": 1}])"); + rootDocument->executeCommands(scrollForwards, false); + advanceTime(1000); + root->clearPending(); + + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + rootDocument->executeCommands(scrollForwards, false); + advanceTime(1000); + root->clearPending(); + + ASSERT_EQ(Point(0, 200), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(1, component->getCalculated(apl::kPropertyAccessibilityActions).size()); + + rapidjson::Document scrollBackwards; + scrollBackwards.Parse(R"([{"type": "Scroll", "componentId": ":root", "distance": -1}])"); + rootDocument->executeCommands(scrollBackwards, false); + advanceTime(1000); + root->clearPending(); + + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + ASSERT_TRUE(CheckDirty(component, + kPropertyAccessibilityActions, kPropertyScrollPosition, + kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getCalculated(apl::kPropertyAccessibilityActions).size()); +} + +static const char *PAGER_DYNAMIC_ACTIONS_FOCUS = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Container", + "direction": "column", + "height": 400, + "width": 100, + "items": [ + { + "type": "Pager", + "id": "focusableChildren", + "height": "25%", + "width": "100%", + "items": { + "id": "${data}Wrapper", + "type": "TouchWrapper" + }, + "data": ["one", "two"] + }, + { + "type": "Pager", + "id": "nonFocusableChildren", + "height": "25%", + "width": "100%", + "items": { + "type": "Frame", + "backgroundColor": "${data}" + }, + "data": ["blue", "red"] + }, + { + "type": "Pager", + "id": "mixedChildren", + "height": "25%", + "width": "100%", + "items": [ + { + "id": "mixedWrapper", + "type": "TouchWrapper" + }, + { + "type": "Frame", + "backgroundColor": "red" + } + ] + }, + { + "type": "Pager", + "id": "deepChildren", + "height": "25%", + "width": "100%", + "items": [ + { + "type": "Frame", + "backgroundColor": "${data}", + "item": { + "id": "${data}Wrapper", + "type": "TouchWrapper", + "height": "100%", + "width": "100%" + }, + "height": "100%", + "width": "100%" + } + ], + "data": ["blue", "red"] + } + ] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, PagerDynamicActionsFocus) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(PAGER_DYNAMIC_ACTIONS_FOCUS); + ASSERT_TRUE(component); + + advanceTime(10); + + auto fcPager = component->getCoreChildAt(0); + auto nfcPager = component->getCoreChildAt(1); + auto mcPager = component->getCoreChildAt(2); + auto dcPager = component->getCoreChildAt(3); + + ASSERT_EQ(kComponentTypePager, fcPager->getType()); + ASSERT_EQ(kComponentTypePager, nfcPager->getType()); + ASSERT_EQ(kComponentTypePager, mcPager->getType()); + ASSERT_EQ(kComponentTypePager, dcPager->getType()); + auto& fm = root->context().focusManager(); + + ASSERT_FALSE(fm.getFocus()); + + // Accessibility page switch should switch to the next focusable child on the new page + root->setFocus(apl::kFocusDirectionNone, Rect(), "oneWrapper"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("oneWrapper"), event.getComponent()); + + fcPager->update(kUpdateAccessibilityAction, "scrollforward"); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("twoWrapper"), event.getComponent()); + ASSERT_EQ(root->findComponentById("twoWrapper"), fm.getFocus()); + + + // Focused pager don't move focus + root->setFocus(apl::kFocusDirectionNone, Rect(), "nonFocusableChildren"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(nfcPager, event.getComponent()); + + nfcPager->update(kUpdateAccessibilityAction, "scrollforward"); + + ASSERT_FALSE(root->hasEvent()); + ASSERT_EQ(nfcPager, fm.getFocus()); + + + // Switch to the page without focusable leads to pager focus + root->setFocus(apl::kFocusDirectionNone, Rect(), "mixedWrapper"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("mixedWrapper"), event.getComponent()); + + mcPager->update(kUpdateAccessibilityAction, "scrollforward"); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(mcPager, event.getComponent()); + ASSERT_EQ(mcPager, fm.getFocus()); + + + // Deeper children switches work similarly to directs + root->setFocus(apl::kFocusDirectionNone, Rect(), "blueWrapper"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("blueWrapper"), event.getComponent()); + + dcPager->update(kUpdateAccessibilityAction, "scrollforward"); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("redWrapper"), event.getComponent()); + ASSERT_EQ(root->findComponentById("redWrapper"), fm.getFocus()); +} + +static const char *SEQUENCE_DYNAMIC_ACTIONS_FOCUS = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Sequence", + "height": 100, + "width": 100, + "items": [ + { + "type": "Frame", + "height": "100%", + "width": "100%", + "items": { + "id": "deepWrapperStart", + "type": "TouchWrapper", + "height": "100%", + "width": "100%" + } + }, + { + "type": "Frame", + "height": "100%", + "width": "100%", + "items": { + "id": "deepWrapperEnd", + "type": "TouchWrapper", + "height": "100%", + "width": "100%" + } + }, + { + "type": "TouchWrapper", + "id": "shallowWrapperStart", + "height": "100%", + "width": "100%" + }, + { + "type": "Frame", + "id": "emptyFrame", + "height": "100%", + "width": "100%" + }, + { + "type": "TouchWrapper", + "id": "shallowWrapperEnd", + "height": "100%", + "width": "100%" + } + ] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, SequenceDynamicActionsFocus) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(SEQUENCE_DYNAMIC_ACTIONS_FOCUS); + ASSERT_TRUE(component); + + advanceTime(10); + + auto& fm = root->context().focusManager(); + + ASSERT_FALSE(fm.getFocus()); + + root->setFocus(apl::kFocusDirectionNone, Rect(), "deepWrapperStart"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("deepWrapperStart"), event.getComponent()); + ASSERT_EQ(root->findComponentById("deepWrapperStart"), fm.getFocus()); + + + // Accessibility scroll should switch to the next focusable child on the new screen (deep) + component->update(kUpdateAccessibilityAction, "scrollforward"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("deepWrapperEnd"), event.getComponent()); + ASSERT_EQ(root->findComponentById("deepWrapperEnd"), fm.getFocus()); + + + // Accessibility scroll should switch to the next focusable child on the new screen (deep) + component->update(kUpdateAccessibilityAction, "scrollbackward"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("deepWrapperStart"), event.getComponent()); + ASSERT_EQ(root->findComponentById("deepWrapperStart"), fm.getFocus()); + + + // Accessibility scroll should switch to the next focusable child on the new screen (deep) + component->update(kUpdateAccessibilityAction, "scrollforward"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("deepWrapperEnd"), event.getComponent()); + ASSERT_EQ(root->findComponentById("deepWrapperEnd"), fm.getFocus()); + + + // Accessibility scroll should switch to the next focusable child on the new screen (shallow) + component->update(kUpdateAccessibilityAction, "scrollforward"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(root->findComponentById("shallowWrapperStart"), event.getComponent()); + ASSERT_EQ(root->findComponentById("shallowWrapperStart"), fm.getFocus()); + + + // Accessibility scroll should switch to the scrollable if focusable child no available + component->update(kUpdateAccessibilityAction, "scrollforward"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(component, event.getComponent()); + ASSERT_EQ(component, fm.getFocus()); + + + // Accessibility scroll should not switch focus from itself + component->update(kUpdateAccessibilityAction, "scrollforward"); + ASSERT_TRUE(fm.getFocus()); + + ASSERT_EQ(component, fm.getFocus()); +} + +static const char *CUSTOM_ACTIONS_ON_MULTICHILD = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Container", + "direction": "column", + "height": 400, + "width": 100, + "items": [ + { + "type": "Pager", + "id": "pagerio", + "height": "50%", + "width": "100%", + "items": { + "type": "Frame", + "backgroundColor": "${data}" + }, + "data": [ + "blue", + "red" + ], + "actions": [ + { + "name": "quitecustom", + "label": "Quite custom", + "command": { + "type": "SendEvent" + } + } + ] + }, + { + "type": "Sequence", + "id": "sequencio", + "height": "50%", + "width": "100%", + "items": { + "type": "Frame", + "backgroundColor": "${data}", + "height": 200, + "width": "100%" + }, + "data": [ + "blue", + "red" + ], + "actions": [ + { + "name": "verycustom", + "label": "Very custom", + "command": { + "type": "SendEvent" + } + } + ] + } + ] + } + } +})apl"; + +TEST_F(AccessibilityActionTest, CustomActionsOnMultichild) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); + + loadDocument(CUSTOM_ACTIONS_ON_MULTICHILD); + ASSERT_TRUE(component); + + advanceTime(10); + + auto pager = component->getCoreChildAt(0); + auto sequence = component->getCoreChildAt(1); + + ASSERT_EQ(kComponentTypePager, pager->getType()); + ASSERT_EQ(kComponentTypeSequence, sequence->getType()); + + ASSERT_EQ(3, pager->getCalculated(apl::kPropertyAccessibilityActions).size()); + ASSERT_EQ(2, sequence->getCalculated(apl::kPropertyAccessibilityActions).size()); + + pager->update(apl::kUpdateAccessibilityAction, "quitecustom"); + ASSERT_TRUE(CheckSendEvent(root)); + + sequence->update(apl::kUpdateAccessibilityAction, "verycustom"); + ASSERT_TRUE(CheckSendEvent(root)); +} diff --git a/aplcore/unit/component/unittest_component_events.cpp b/aplcore/unit/component/unittest_component_events.cpp index 590c6ea..5e55681 100644 --- a/aplcore/unit/component/unittest_component_events.cpp +++ b/aplcore/unit/component/unittest_component_events.cpp @@ -1163,3 +1163,86 @@ TEST_F(ComponentEventsTest, MediaFastNormal) auto event = root->popEvent(); ASSERT_EQ(kEventTypeSendEvent, event.getType()); } + +static const char *CHILDREN_CHANGED = R"apl({ + "type": "APL", + "version": "2023.3", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Sequence", + "id": "baseContainer", + "width": "100%", + "height": 100, + "items": [], + "onChildrenChanged": { + "type": "Sequential", + "data": "${event.changes}", + "commands": { + "type": "SendEvent", + "sequencer": "SE", + "arguments": [ + "${event.source.handler}", + "${data.index ? data.index : 0}", + "${data.action}" + ], + "components": [ "textComp" ] + } + } + } + } +})apl"; + +TEST_F(ComponentEventsTest, ChildrenChanged) +{ + loadDocument(CHILDREN_CHANGED); + ASSERT_TRUE(component); + + rapidjson::Document doc; + doc.Parse(R"([ + { + "type": "InsertItem", + "componentId": "baseContainer", + "at": 10, + "item": { + "type": "Frame", + "height": 200, + "width": "100%" + } + } + ])"); + + executeCommands(apl::Object(doc), false); + + root->clearPending(); + advanceTime(500); + + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 0, "insert")); + + executeCommands(apl::Object(doc), false); + + root->clearPending(); + advanceTime(500); + + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 1, "insert")); + + + executeCommands(apl::Object(doc), false); + executeCommands(apl::Object(doc), false); + + doc.Parse(R"apl([ + { + "type": "RemoveItem", + "componentId": "baseContainer:child(0)" + } + ])apl"); + + executeCommands(apl::Object(doc), false); + + root->clearPending(); + advanceTime(500); + + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 1, "insert")); + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 2, "insert")); + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 0, "remove")); +} diff --git a/aplcore/unit/component/unittest_edit_text_component.cpp b/aplcore/unit/component/unittest_edit_text_component.cpp index b4b872b..29f7e61 100644 --- a/aplcore/unit/component/unittest_edit_text_component.cpp +++ b/aplcore/unit/component/unittest_edit_text_component.cpp @@ -945,3 +945,65 @@ TEST_F(EditTextComponentTest, NoOpWhenAlreadyInFocus) { ASSERT_FALSE(root->hasEvent()); } +static const char* EDIT_TEXT_IN_CONTAINER = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "items": { + "type": "Container", + "width": 200, + "height": 200, + "items": { + "type": "EditText", + "id": "EDITTEXT", + "width": "100%", + "height": "100%" + } + } + } +})"; + +TEST_F(EditTextComponentTest, DisallowEditTextTrueDisallowsComponent) { + config->set(RootProperty::kDisallowEditText, true); + loadDocument(EDIT_TEXT_IN_CONTAINER); + + ASSERT_TRUE(component); + auto et = std::static_pointer_cast(root->findComponentById("EDITTEXT")); + ASSERT_EQ(et->isDisallowed(), true); +} + +TEST_F(EditTextComponentTest, DisallowEditTextFalseAllowsComponent) { + config->set(RootProperty::kDisallowEditText, false); + loadDocument(EDIT_TEXT_IN_CONTAINER); + + ASSERT_TRUE(component); + auto et = std::static_pointer_cast(root->findComponentById("EDITTEXT")); + ASSERT_EQ(et->isDisallowed(), false); +} + +TEST_F(EditTextComponentTest, ComponentNotDisplayedWhenDisallowEditTextTrue) { + config->set(RootProperty::kDisallowEditText, true); + + loadDocument(EDIT_TEXT_IN_CONTAINER); + + ASSERT_TRUE(component); + // Components are inflated as expected + ASSERT_EQ(1, component->getChildCount()); + ASSERT_EQ(kComponentTypeEditText, component->getCoreChildAt(0)->getType()); + // Not displayed + ASSERT_EQ(0, component->getDisplayedChildCount()); +} + +TEST_F(EditTextComponentTest, ComponentDisplayedWhenDisallowEditTextFalse) { + config->set(RootProperty::kDisallowEditText, false); + + loadDocument(EDIT_TEXT_IN_CONTAINER); + + ASSERT_TRUE(component); + // Components are inflated as expected + ASSERT_EQ(1, component->getChildCount()); + ASSERT_EQ(kComponentTypeEditText, component->getCoreChildAt(0)->getType()); + // Displayed + ASSERT_EQ(1, component->getDisplayedChildCount()); + ASSERT_EQ(kComponentTypeEditText, component->getDisplayedChildAt(0)->getType()); +} \ No newline at end of file diff --git a/aplcore/unit/component/unittest_frame_component.cpp b/aplcore/unit/component/unittest_frame_component.cpp index 64cd36a..ed4ad26 100644 --- a/aplcore/unit/component/unittest_frame_component.cpp +++ b/aplcore/unit/component/unittest_frame_component.cpp @@ -230,6 +230,7 @@ TEST_F(FrameComponentTest, Styled) { // all values are styled ASSERT_TRUE(IsEqual(Color(Color::YELLOW), frame->getCalculated(kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Color(Color::YELLOW), frame->getCalculated(kPropertyBackground))); ASSERT_TRUE(IsEqual(Color(Color::BLUE), frame->getCalculated(kPropertyBorderColor))); ASSERT_TRUE(IsEqual(Dimension(10), frame->getCalculated(kPropertyBorderRadius))); @@ -574,3 +575,309 @@ TEST_F(FrameComponentTest, StyleFrameInnerBounds) ASSERT_EQ(Rect(100, 100, width - 200, height - 200), component->getCalculated(kPropertyInnerBounds).get()); ASSERT_EQ(Rect(100, 100, width - 400, height - 400), image->getCalculated(kPropertyInnerBounds).get()); } + +static const char *FRAME_BACKGROUND_OPTIONS = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "items": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Frame", + "width": "33%", + "height": "33%", + "backgroundColor": "red" + }, + { + "type": "Frame", + "width": "33%", + "height": "33%", + "background": "red" + }, + { + "type": "Frame", + "width": "33%", + "height": "33%", + "background": { + "type": "linear", + "colorRange": [ "#FF000066", "#F7C10066", "#6DD40066", "#0091FF66", "#6236FF66"], + "inputRange": [ 0, 0.25, 0.55, 0.8, 1.0 ], + "angle": 270 + } + } + ] + } + } +})"; + +TEST_F(FrameComponentTest, FrameBackgroundOptions) +{ + loadDocument(FRAME_BACKGROUND_OPTIONS); + + ASSERT_EQ(Color(0xff0000ff), component->getChildAt(0)->getCalculated(apl::kPropertyBackground).asColor(session)); + ASSERT_EQ(Color(0xff0000ff), component->getChildAt(1)->getCalculated(apl::kPropertyBackground).asColor(session)); + auto gradient = component->getChildAt(2)->getCalculated(apl::kPropertyBackground).get(); + ASSERT_EQ(270, gradient.getProperty(apl::kGradientPropertyAngle).getInteger()); +} + +static const char *FRAME_BACKGROUND_OVERRIDE = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "items": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "red", + "background": "blue" + } + } + } +})"; + +TEST_F(FrameComponentTest, FrameBackgroundOverride) +{ + loadDocument(FRAME_BACKGROUND_OVERRIDE); + + ASSERT_EQ(Color(0x0000ffff), component->getCalculated(apl::kPropertyBackground).asColor(session)); +} + +static const char *STYLE_FRAME_BACKGROUND_FROM_COLOR = R"({ + "type": "APL", + "version": "2023.3", + "styles": { + "FrameStyle": { + "values": [ + { + "backgroundColor": "red" + }, + { + "when": "${state.pressed}", + "background": "blue" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "Frame", + "style": "FrameStyle", + "width": "100%", + "height": "100%" + } + } +})"; + +TEST_F(FrameComponentTest, StyleFrameBackgroundFromColor) +{ + loadDocument(STYLE_FRAME_BACKGROUND_FROM_COLOR); + + ASSERT_TRUE(component->getCalculated(apl::kPropertyBackground).is()); + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackground).getColor()); + + component->setState(kStatePressed, true); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyBackground, kPropertyVisualHash)); + + ASSERT_TRUE(component->getCalculated(apl::kPropertyBackground).is()); + ASSERT_EQ(0x0000ffff, component->getCalculated(apl::kPropertyBackground).getColor()); +} + +static const char *STYLE_FRAME_BACKGROUND_TO_GRADIENT = R"({ + "type": "APL", + "version": "2023.3", + "styles": { + "FrameStyle": { + "values": [ + { + "background": "red" + }, + { + "when": "${state.pressed}", + "background": { + "type": "linear", + "colorRange": [ "#FF000066", "#F7C10066", "#6DD40066", "#0091FF66", "#6236FF66"], + "inputRange": [ 0, 0.25, 0.55, 0.8, 1.0 ], + "angle": 270 + } + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "Frame", + "style": "FrameStyle", + "width": "100%", + "height": "100%" + } + } +})"; + +TEST_F(FrameComponentTest, StyleFrameBackgroundToGradient) +{ + loadDocument(STYLE_FRAME_BACKGROUND_TO_GRADIENT); + + ASSERT_TRUE(component->getCalculated(apl::kPropertyBackground).is()); + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackground).getColor()); + + component->setState(kStatePressed, true); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyBackground, kPropertyVisualHash)); + + ASSERT_TRUE(component->getCalculated(apl::kPropertyBackground).is()); +} + +static const char *STYLE_FRAME_COLOR_TO_BACKGROUND_OVERRIDE = R"({ + "type": "APL", + "version": "2023.3", + "styles": { + "FrameStyle": { + "values": [ + { + "background": "red" + }, + { + "when": "${state.pressed}", + "backgroundColor": "green", + "background": "blue" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "Frame", + "style": "FrameStyle", + "width": "100%", + "height": "100%" + } + } +})"; + +TEST_F(FrameComponentTest, StyleFrameColorToBackgroundOverride) +{ + loadDocument(STYLE_FRAME_COLOR_TO_BACKGROUND_OVERRIDE); + + ASSERT_TRUE(component->getCalculated(apl::kPropertyBackground).is()); + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackground).getColor()); + + component->setState(kStatePressed, true); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyBackgroundColor, kPropertyBackground, kPropertyVisualHash)); + + ASSERT_EQ(0x0000ffff, component->getCalculated(apl::kPropertyBackground).getColor()); +} + +static const char *STYLE_FRAME_BACKGROUND_TO_COLOR = R"({ + "type": "APL", + "version": "2023.3", + "styles": { + "FrameStyle": { + "values": [ + { + "background": "red" + }, + { + "when": "${state.pressed}", + "backgroundColor": "green" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "Frame", + "style": "FrameStyle", + "width": "100%", + "height": "100%" + } + } +})"; + +TEST_F(FrameComponentTest, StyleFrameBackgroundToColor) +{ + loadDocument(STYLE_FRAME_BACKGROUND_TO_COLOR); + + ASSERT_TRUE(component->getCalculated(apl::kPropertyBackground).is()); + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackground).getColor()); + + component->setState(kStatePressed, true); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyBackgroundColor, kPropertyVisualHash)); + + // Can't override preferred "background", which is defined in the base style. + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackground).getColor()); +} + +static const char *STYLE_FRAME_COLOR_TO_TRANSPARENT_BACKGROUND_OVERRIDE = R"({ + "type": "APL", + "version": "2023.3", + "styles": { + "FrameStyle": { + "values": [ + { + "backgroundColor": "red" + }, + { + "when": "${state.pressed}", + "backgroundColor": "red", + "background": "transparent" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "Frame", + "style": "FrameStyle", + "width": "100%", + "height": "100%" + } + } +})"; + +TEST_F(FrameComponentTest, StyleFrameColorToTransparentBackgroundOverride) +{ + loadDocument(STYLE_FRAME_COLOR_TO_TRANSPARENT_BACKGROUND_OVERRIDE); + + ASSERT_TRUE(component->getCalculated(apl::kPropertyBackground).is()); + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackground).getColor()); + + component->setState(kStatePressed, true); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyBackground, kPropertyVisualHash)); + + ASSERT_EQ(0x00000000, component->getCalculated(apl::kPropertyBackground).getColor()); +} + +static const char *FRAME_COLOR_TO_TRANSPARENT_BACKGROUND_OVERRIDE = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "items": { + "type": "Frame", + "style": "FrameStyle", + "width": "100%", + "height": "100%", + "backgroundColor": "red", + "background": "transparent" + } + } +})"; + +TEST_F(FrameComponentTest, FrameColorToTransparentBackgroundOverride) +{ + loadDocument(FRAME_COLOR_TO_TRANSPARENT_BACKGROUND_OVERRIDE); + + ASSERT_TRUE(component->getCalculated(apl::kPropertyBackground).is()); + ASSERT_EQ(0x00000000, component->getCalculated(apl::kPropertyBackground).getColor()); +} diff --git a/aplcore/unit/component/unittest_host_component.cpp b/aplcore/unit/component/unittest_host_component.cpp index d581213..2c57674 100644 --- a/aplcore/unit/component/unittest_host_component.cpp +++ b/aplcore/unit/component/unittest_host_component.cpp @@ -233,6 +233,10 @@ TEST_F(HostComponentTest, TestSuccessAndFailDoNothingAfterDelete) weak = host; host = nullptr; } + + root->clearPending(); + root->clearDirty(); + // nobody has a reference to "host" anymore ASSERT_TRUE(weak.lock() == nullptr); @@ -352,7 +356,7 @@ TEST_F(HostComponentTest, TestSetSourcePropertyCancelsRequestAndNewRequestFails) ASSERT_TRUE(root->findComponentById(onFailArtifactId)); } -TEST_F(HostComponentTest, TestResolvedContentWithPendingParamterSuccess) +TEST_F(HostComponentTest, TestResolvedContentWithPendingParameterSuccess) { auto content = Content::create( R"({ @@ -383,9 +387,7 @@ TEST_F(HostComponentTest, TestResolvedContentWithPendingParamterSuccess) ASSERT_FALSE(root->findComponentById("onFailArtifact")); auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); - // onLoadHandle runs before the pending parameters are resolved, so its artifact is not sufficient ASSERT_TRUE(root->findComponentById("onLoadArtifact")); - // If the pending parameter is not resolved, onFail would be invoked; verify onFail did not run ASSERT_FALSE(root->findComponentById("onFailArtifact")); ASSERT_EQ( std::static_pointer_cast( @@ -393,7 +395,7 @@ TEST_F(HostComponentTest, TestResolvedContentWithPendingParamterSuccess) "Hello, World!"); } -TEST_F(HostComponentTest, TestResolvedContentWithPendingParamterFailure) +TEST_F(HostComponentTest, TestResolvedContentWithMissingParameterBecomesNull) { auto content = Content::create( R"({ @@ -408,7 +410,8 @@ TEST_F(HostComponentTest, TestResolvedContentWithPendingParamterFailure) "type": "Container", "item": { "type": "Text", - "value": "${EmbeddedParameter}" + "id": "embeddedText", + "text": "${EmbeddedParameter} - ${MissingParameter}" } } } @@ -420,18 +423,19 @@ TEST_F(HostComponentTest, TestResolvedContentWithPendingParamterFailure) ASSERT_EQ(pendingParameters.size(), 2); ASSERT_NE(pendingParameters.find("EmbeddedParameter"), pendingParameters.end()); ASSERT_NE(pendingParameters.find("MissingParameter"), pendingParameters.end()); + ASSERT_FALSE(content->isReady()); loadDocument(); ASSERT_FALSE(root->findComponentById("onLoadArtifact")); ASSERT_FALSE(root->findComponentById("onFailArtifact")); - documentManager->succeed("embeddedDocumentUrl", content, true); - ASSERT_TRUE(session->checkAndClear("Missing value for parameter MissingParameter")); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(content->isReady()); - // onLoadHandle runs before the pending parameters are resolved, so its artifact is not sufficient ASSERT_TRUE(root->findComponentById("onLoadArtifact")); - // onFailHandler should run because the "Missing" parameter will not be resolved - ASSERT_TRUE(root->findComponentById("onFailArtifact")); + ASSERT_FALSE(root->findComponentById("onFailArtifact")); + auto embeddedTextComponent = CoreDocumentContext::cast(embeddedDoc)->findComponentById("embeddedText"); + ASSERT_EQ("Hello, World! - ", embeddedTextComponent->getCalculated(kPropertyText).asString()); } TEST_F(HostComponentTest, TestFindComponentByIdTraversingHostForHostById) @@ -582,3 +586,524 @@ TEST_F(HostComponentTest, TestHostSizeChangeSendsConfigurationChangeToEmbedded) ASSERT_NE(embeddedTopInitialBounds, embeddedNewBounds); ASSERT_EQ(hostNewBounds, embeddedNewBounds); } + +static const char* HOST_ENVIRONMENT_ENV_DISALLOW_TRUE = R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "width": 200, + "height": 200, + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "environment": { + "disallowEditText": true, + "disallowVideo": true + } + } + } + } +})"; + +static const char* HOST_ENVIRONMENT_ENV_DISALLOW_FALSE = R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "width": 200, + "height": 200, + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "environment": { + "disallowEditText": false, + "disallowVideo": false + } + } + } + } +})"; + +static const char* EDIT_TEXT_EMBEDDED = R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "id": "embeddedTop", + "item": { + "type": "EditText", + "width": "100%", + "height": "100%", + "id": "embeddedEditText" + } + } + } +})"; + +static const char* VIDEO_EMBEDDED = R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "id": "embeddedTop", + "item": { + "type": "Video", + "width": "100%", + "height": "100%", + "id": "embeddedVideo" + } + } + } +})"; + +TEST_F(HostComponentTest, EmbeddedEditTextNotDisplayedWhenEmbeddedDisallowEditTextTrue) { + config->set(RootProperty::kDisallowEditText, false); + loadDocument(HOST_ENVIRONMENT_ENV_DISALLOW_TRUE); + + auto content = Content::create(EDIT_TEXT_EMBEDDED, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + advanceTime(10); + + ASSERT_EQ(host->getChildCount(), 1); + auto c = host->getChildAt(0); + ASSERT_EQ(kComponentTypeContainer, c->getType()); + // Component not displayed + ASSERT_EQ(1, c->getChildCount()); + ASSERT_EQ(kComponentTypeEditText, c->getChildAt(0)->getType()); + ASSERT_EQ(0, c->getDisplayedChildCount()); +} + +TEST_F(HostComponentTest, EmbeddedEditTextDisplayedWhenEmbeddedDisallowEditTextFalse) { + config->set(RootProperty::kDisallowEditText, false); + loadDocument(HOST_ENVIRONMENT_ENV_DISALLOW_FALSE); + + auto content = Content::create(EDIT_TEXT_EMBEDDED, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + advanceTime(10); + + ASSERT_EQ(host->getChildCount(), 1); + auto c = host->getChildAt(0); + ASSERT_EQ(kComponentTypeContainer, c->getType()); + // Component displayed + ASSERT_EQ(1, c->getChildCount()); + ASSERT_EQ(kComponentTypeEditText, c->getChildAt(0)->getType()); + ASSERT_EQ(1, c->getDisplayedChildCount()); +} + +TEST_F(HostComponentTest, EmbeddedVideoNotDisplayedWhenEmbeddedDisallowVideoTrue) { + config->set(RootProperty::kDisallowVideo, false); + loadDocument(HOST_ENVIRONMENT_ENV_DISALLOW_TRUE); + + auto content = Content::create(VIDEO_EMBEDDED, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + advanceTime(10); + + ASSERT_EQ(host->getChildCount(), 1); + auto c = host->getChildAt(0); + ASSERT_EQ(kComponentTypeContainer, c->getType()); + // Component not displayed + ASSERT_EQ(1, c->getChildCount()); + ASSERT_EQ(kComponentTypeVideo, c->getChildAt(0)->getType()); + ASSERT_EQ(0, c->getDisplayedChildCount()); +} + +TEST_F(HostComponentTest, EmbeddedVideoDisplayedWhenEmbeddedDisallowVideoFalse) { + config->set(RootProperty::kDisallowVideo, false); + loadDocument(HOST_ENVIRONMENT_ENV_DISALLOW_FALSE); + + auto content = Content::create(VIDEO_EMBEDDED, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + advanceTime(10); + + ASSERT_EQ(host->getChildCount(), 1); + auto c = host->getChildAt(0); + ASSERT_EQ(kComponentTypeContainer, c->getType()); + // Component displayed + ASSERT_EQ(1, c->getChildCount()); + ASSERT_EQ(kComponentTypeVideo, c->getChildAt(0)->getType()); + ASSERT_EQ(1, c->getDisplayedChildCount()); +} + +static const char* EXPLICIT_PARAMETER_HOST = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "Container", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "parameters": { + "ExplicitParameter": "Hello, World!" + }, + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "LOAD_SEQUENCER", + "arguments": "Loaded" + } + ], + "onFail": [ + { + "type": "SendEvent", + "sequencer": "FAIL_SEQUENCER", + "arguments": "Failed" + } + ] + } + } + } +})"; + +static const char* EXPLICIT_PARAMETER_EMBEDDED = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "parameters": "ExplicitParameter", + "item": { + "type": "Container", + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${ExplicitParameter}" + } + } + } +})"; + +TEST_F(HostComponentTest, TestExplicitParameterPassing) +{ + loadDocument(EXPLICIT_PARAMETER_HOST); + ASSERT_TRUE(host); + ASSERT_FALSE(CheckSendEvent(root, "Loaded")); + ASSERT_FALSE(CheckSendEvent(root, "Failed")); + + auto content = Content::create(EXPLICIT_PARAMETER_EMBEDDED, makeDefaultSession()); + + std::set pendingParameters = content->getPendingParameters(); + ASSERT_EQ(pendingParameters.size(), 1); + ASSERT_NE(pendingParameters.find("ExplicitParameter"), pendingParameters.end()); + + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "Loaded")); + ASSERT_FALSE(CheckSendEvent(root, "Failed")); + + auto embeddedTextComponent = CoreDocumentContext::cast(embeddedDoc)->findComponentById("embeddedText"); + ASSERT_EQ("Hello, World!", embeddedTextComponent->getCalculated(kPropertyText).asString()); + + // Verify plural version of parameter is chosen in DOM serialization + rapidjson::Document doc; + auto json = root->serializeDOM(true, doc.GetAllocator()); + auto& hostJson = json["children"][0]; + auto& param = hostJson["parameters"]["ExplicitParameter"]; + ASSERT_EQ(std::string("Hello, World!"), param.GetString()); +} + +static const char* EXPLICIT_PARAMETER_HOST_WITH_PLURAL_AND_SINGULAR = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "Container", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "parameter": { + "SingularOnly": "One", + "Both": "SingularWins" + }, + "parameters": { + "PluralOnly": "Many", + "Both": "PluralWins" + } + } + } + } +})"; + +static const char* EXPLICIT_PARAMETER_EMBEDDED_WITH_PLURAL_AND_SINGULAR = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "parameters": [ + "SingularOnly", + "PluralOnly", + "Both" + ], + "item": { + "type": "Container", + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${SingularOnly} - ${PluralOnly} - ${Both}" + } + } + } +})"; + +TEST_F(HostComponentTest, TestPluralVariantPreferred) +{ + loadDocument(EXPLICIT_PARAMETER_HOST_WITH_PLURAL_AND_SINGULAR); + ASSERT_TRUE(host); + + auto content = Content::create(EXPLICIT_PARAMETER_EMBEDDED_WITH_PLURAL_AND_SINGULAR, makeDefaultSession()); + + std::set pendingParameters = content->getPendingParameters(); + ASSERT_EQ(pendingParameters.size(), 3); + ASSERT_NE(pendingParameters.find("SingularOnly"), pendingParameters.end()); + ASSERT_NE(pendingParameters.find("PluralOnly"), pendingParameters.end()); + ASSERT_NE(pendingParameters.find("Both"), pendingParameters.end()); + + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + auto embeddedTextComponent = CoreDocumentContext::cast(embeddedDoc)->findComponentById("embeddedText"); + ASSERT_EQ(" - Many - PluralWins", embeddedTextComponent->getCalculated(kPropertyText).asString()); + + // Verify plural version of parameter is chosen in DOM serialization + rapidjson::Document doc; + auto json = root->serializeDOM(true, doc.GetAllocator()); + auto& hostJson = json["children"][0]; + ASSERT_FALSE(hostJson.HasMember("parameter")); + ASSERT_TRUE(hostJson.HasMember("parameters")); + ASSERT_EQ(std::string("Many"), hostJson["parameters"]["PluralOnly"].GetString()); + ASSERT_EQ(std::string("PluralWins"), hostJson["parameters"]["Both"].GetString()); + ASSERT_FALSE(hostJson["parameters"].HasMember("SinglarOnly")); +} + +static const char* EXPLICIT_PARAMETER_HOST_WITH_SINGULAR = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "Container", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "parameter": { + "Singular": "One" + } + } + } + } +})"; + +static const char* EXPLICIT_PARAMETER_EMBEDDED_WITH_SINGULAR = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "parameters": [ + "Singular" + ], + "item": { + "type": "Container", + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${Singular}" + } + } + } +})"; + +TEST_F(HostComponentTest, TestSingularVariantWorks) +{ + loadDocument(EXPLICIT_PARAMETER_HOST_WITH_SINGULAR); + ASSERT_TRUE(host); + + auto content = Content::create(EXPLICIT_PARAMETER_EMBEDDED_WITH_SINGULAR, makeDefaultSession()); + + std::set pendingParameters = content->getPendingParameters(); + ASSERT_EQ(pendingParameters.size(), 1); + ASSERT_NE(pendingParameters.find("Singular"), pendingParameters.end()); + + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + auto embeddedTextComponent = CoreDocumentContext::cast(embeddedDoc)->findComponentById("embeddedText"); + ASSERT_EQ("One", embeddedTextComponent->getCalculated(kPropertyText).asString()); + + // Verify plural version of parameter is chosen in DOM serialization + rapidjson::Document doc; + auto json = root->serializeDOM(true, doc.GetAllocator()); + auto& hostJson = json["children"][0]; + auto& param = hostJson["parameters"]["Singular"]; + ASSERT_EQ(std::string("One"), param.GetString()); +} + +static const char* EXPLICIT_AND_IMPLICIT_PARAMETER_HOST = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "Container", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "ImplicitParameter": "Implicit value", + "parameters": { + "ExplicitParameter": "Explicit value" + }, + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "LOAD_SEQUENCER", + "arguments": "Loaded" + } + ], + "onFail": [ + { + "type": "SendEvent", + "sequencer": "FAIL_SEQUENCER", + "arguments": "Failed" + } + ] + } + } + } +})"; + +static const char* EXPLICIT_AND_IMPLICIT_PARAMETER_EMBEDDED = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "parameters": [ + "ExplicitParameter", + "ImplicitParameter" + ], + "item": { + "type": "Container", + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${ExplicitParameter} - ${ImplicitParameter}" + } + } + } +})"; + +TEST_F(HostComponentTest, TestDisallowImplicitParametersWhenUsingExplicitParameters) +{ + loadDocument(EXPLICIT_AND_IMPLICIT_PARAMETER_HOST); + ASSERT_TRUE(host); + ASSERT_FALSE(CheckSendEvent(root, "Loaded")); + ASSERT_FALSE(CheckSendEvent(root, "Failed")); + + auto content = Content::create(EXPLICIT_AND_IMPLICIT_PARAMETER_EMBEDDED, makeDefaultSession()); + + std::set pendingParameters = content->getPendingParameters(); + ASSERT_EQ(pendingParameters.size(), 2); + ASSERT_NE(pendingParameters.find("ExplicitParameter"), pendingParameters.end()); + ASSERT_NE(pendingParameters.find("ImplicitParameter"), pendingParameters.end()); + + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "Loaded")); + ASSERT_FALSE(CheckSendEvent(root, "Failed")); + + auto embeddedTextComponent = CoreDocumentContext::cast(embeddedDoc)->findComponentById("embeddedText"); + ASSERT_EQ("Explicit value - ", embeddedTextComponent->getCalculated(kPropertyText).asString()); +} + +static const char* IMPLICIT_INTRINSIC_PROPERTY_PARAMETER_HOST = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "Container", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "ImplicitParameter": "Implicit value", + "speech": "URL", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "LOAD_SEQUENCER", + "arguments": "Loaded" + } + ], + "onFail": [ + { + "type": "SendEvent", + "sequencer": "FAIL_SEQUENCER", + "arguments": "Failed" + } + ] + } + } + } +})"; + +static const char* IMPLICIT_INTRINSIC_PROPERTY_PARAMETER_EMBEDDED = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "parameters": [ + "ImplicitParameter", + "speech" + ], + "item": { + "type": "Container", + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${ImplicitParameter} - ${speech}" + } + } + } +})"; + +TEST_F(HostComponentTest, TestDisallowReadingIntrinsicPropertiesAsImplicitParameters) +{ + loadDocument(IMPLICIT_INTRINSIC_PROPERTY_PARAMETER_HOST); + ASSERT_TRUE(host); + ASSERT_FALSE(CheckSendEvent(root, "Loaded")); + ASSERT_FALSE(CheckSendEvent(root, "Failed")); + + auto content = Content::create(IMPLICIT_INTRINSIC_PROPERTY_PARAMETER_EMBEDDED, makeDefaultSession()); + + std::set pendingParameters = content->getPendingParameters(); + ASSERT_EQ(pendingParameters.size(), 2); + ASSERT_NE(pendingParameters.find("ImplicitParameter"), pendingParameters.end()); + ASSERT_NE(pendingParameters.find("speech"), pendingParameters.end()); + + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "Loaded")); + ASSERT_FALSE(CheckSendEvent(root, "Failed")); + + auto embeddedTextComponent = CoreDocumentContext::cast(embeddedDoc)->findComponentById("embeddedText"); + ASSERT_EQ("Implicit value - ", embeddedTextComponent->getCalculated(kPropertyText).asString()); +} + +TEST_F(HostComponentTest, ExperimentalFeaturesCopiedCorrectly) { + nominalLoadHostAndEmbedded(); + auto child = host->getChildAt(0); + auto hostExperimentalFeatures = host->getContext()->getRootConfig().getExperimentalFeatures(); + auto childExperimentalFeatures = child->getContext()->getRootConfig().getExperimentalFeatures(); + ASSERT_EQ(hostExperimentalFeatures, childExperimentalFeatures); +} + diff --git a/aplcore/unit/component/unittest_scroll.cpp b/aplcore/unit/component/unittest_scroll.cpp index 1b11c60..c0e8fc2 100644 --- a/aplcore/unit/component/unittest_scroll.cpp +++ b/aplcore/unit/component/unittest_scroll.cpp @@ -33,6 +33,17 @@ class ScrollTest : public DocumentWrapper { executeCommands(doc, false); } + void executeScroll(const std::string& component, float distance, int duration) { + rapidjson::Value cmd(rapidjson::kObjectType); + auto& alloc = doc.GetAllocator(); + cmd.AddMember("type", "Scroll", alloc); + cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); + cmd.AddMember("distance", distance, alloc); + cmd.AddMember("targetDuration", duration, alloc); + doc.SetArray().PushBack(cmd, alloc); + executeCommands(doc, false); + } + void executeScroll(const std::string& component, const std::string& distance) { rapidjson::Value cmd(rapidjson::kObjectType); auto& alloc = doc.GetAllocator(); @@ -49,6 +60,12 @@ class ScrollTest : public DocumentWrapper { advanceTime(1000); } + void completeScroll(const ComponentPtr& component, float distance, int duration) { + ASSERT_FALSE(root->hasEvent()); + executeScroll(component->getId(), distance, duration); + advanceTime(duration); + } + void completeScroll(const ComponentPtr& component, const std::string& distance) { ASSERT_FALSE(root->hasEvent()); executeScroll(component->getId(), distance); @@ -66,12 +83,30 @@ class ScrollTest : public DocumentWrapper { executeCommands(doc, false); } + void executeScrollToIndex(const std::string& component, int index, CommandScrollAlign align, int duration) { + rapidjson::Value cmd(rapidjson::kObjectType); + auto& alloc = doc.GetAllocator(); + cmd.AddMember("type", "ScrollToIndex", alloc); + cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); + cmd.AddMember("index", index, alloc); + cmd.AddMember("align", rapidjson::StringRef(sCommandAlignMap.at(align).c_str()), alloc); + cmd.AddMember("targetDuration", duration, alloc); + doc.SetArray().PushBack(cmd, alloc); + executeCommands(doc, false); + } + void scrollToIndex(const ComponentPtr& component, int index, CommandScrollAlign align) { ASSERT_FALSE(root->hasEvent()); executeScrollToIndex(component->getId(), index, align); advanceTime(1000); } + void scrollToIndex(const ComponentPtr& component, int index, CommandScrollAlign align, int duration) { + ASSERT_FALSE(root->hasEvent()); + executeScrollToIndex(component->getId(), index, align, duration); + advanceTime(duration); + } + void executeScrollToComponent(const std::string& component, CommandScrollAlign align) { rapidjson::Value cmd(rapidjson::kObjectType); auto& alloc = doc.GetAllocator(); @@ -82,51 +117,67 @@ class ScrollTest : public DocumentWrapper { executeCommands(doc, false); } + void executeScrollToComponent(const std::string& component, CommandScrollAlign align, int duration) { + rapidjson::Value cmd(rapidjson::kObjectType); + auto& alloc = doc.GetAllocator(); + cmd.AddMember("type", "ScrollToComponent", alloc); + cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); + cmd.AddMember("align", rapidjson::StringRef(sCommandAlignMap.at(align).c_str()), alloc); + cmd.AddMember("targetDuration", duration, alloc); + doc.SetArray().PushBack(cmd, alloc); + executeCommands(doc, false); + } + void scrollToComponent(const ComponentPtr& component, CommandScrollAlign align) { ASSERT_FALSE(root->hasEvent()); executeScrollToComponent(component->getId(), align); advanceTime(1000); } + void scrollToComponent(const ComponentPtr& component, CommandScrollAlign align, int duration) { + ASSERT_FALSE(root->hasEvent()); + executeScrollToComponent(component->getId(), align, duration); + advanceTime(duration); + } + rapidjson::Document doc; }; -static const char *SCROLL_TEST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Container\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": [" - " {" - " \"type\": \"ScrollView\"," - " \"id\": \"myScrollView\"," - " \"width\": \"200\"," - " \"height\": \"200\"," - " \"items\": {" - " \"type\": \"Frame\"," - " \"id\": \"myFrame\"," - " \"width\": 200," - " \"height\": 1000" - " }" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"myTouch\"," - " \"height\": 10," - " \"onPress\": {" - " \"type\": \"Scroll\"," - " \"componentId\": \"myScrollView\"," - " \"distance\": 0.5" - " }" - " }" - " ]" - " }" - " }" - "}"; +static const char *SCROLL_TEST = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Container", + "width": 200, + "height": 300, + "items": [ + { + "type": "ScrollView", + "id": "myScrollView", + "width": "200", + "height": "200", + "items": { + "type": "Frame", + "id": "myFrame", + "width": 200, + "height": 1000 + } + }, + { + "type": "TouchWrapper", + "id": "myTouch", + "height": 10, + "onPress": { + "type": "Scroll", + "componentId": "myScrollView", + "distance": 0.5 + } + } + ] + } + } +})"; TEST_F(ScrollTest, ScrollForward) { @@ -264,27 +315,26 @@ TEST_F(ScrollTest, ScrollTextWithAlignmentNoScrolling) advanceTime(1000); } -static const char *SCROLLVIEW_WITH_PADDING = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"ScrollView\"," - " \"id\": \"myScrollView\"," - " \"paddingTop\": 50," - " \"paddingBottom\": 50," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"id\": \"myFrame\"," - " \"width\": 100," - " \"height\": 1000" - " }" - " }" - " }" - "}"; +static const char *SCROLLVIEW_WITH_PADDING = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "ScrollView", + "id": "myScrollView", + "paddingTop": 50, + "paddingBottom": 50, + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "id": "myFrame", + "width": 100, + "height": 1000 + } + } + } +})"; TEST_F(ScrollTest, ScrollViewPadding) { @@ -308,27 +358,26 @@ TEST_F(ScrollTest, ScrollViewPadding) ASSERT_EQ(Rect(0, -750, 100, 1000), frame->getGlobalBounds()); } -static const char *SCROLLVIEW_SMALL = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"ScrollView\"," - " \"id\": \"myScrollView\"," - " \"paddingTop\": 50," - " \"paddingBottom\": 50," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"id\": \"myFrame\"," - " \"width\": 100," - " \"height\": 50" - " }" - " }" - " }" - "}"; +static const char *SCROLLVIEW_SMALL = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "ScrollView", + "id": "myScrollView", + "paddingTop": 50, + "paddingBottom": 50, + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "id": "myFrame", + "width": 100, + "height": 50 + } + } + } +})"; TEST_F(ScrollTest, ScrollViewSmall) { @@ -348,21 +397,20 @@ TEST_F(ScrollTest, ScrollViewSmall) ASSERT_EQ(Rect(0, 50, 100, 50), frame->getGlobalBounds()); } -static const char *SCROLLVIEW_NONE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"ScrollView\"," - " \"id\": \"myScrollView\"," - " \"paddingTop\": 50," - " \"paddingBottom\": 50," - " \"width\": 200," - " \"height\": 300" - " }" - " }" - "}"; +static const char *SCROLLVIEW_NONE = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "ScrollView", + "id": "myScrollView", + "paddingTop": 50, + "paddingBottom": 50, + "width": 200, + "height": 300 + } + } +})"; TEST_F(ScrollTest, ScrollViewNone) { @@ -379,37 +427,25 @@ TEST_F(ScrollTest, ScrollViewNone) } -static const char *SEQUENCE_TEST_HORIZONTAL = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"horizontal\"," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 100" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6," - " 7," - " 8," - " 9," - " 10" - " ]" - " }" - " }" - "}"; +static const char *SEQUENCE_TEST_HORIZONTAL = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "horizontal", + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "width": 100, + "height": 100 + }, + "data": "${Array.range(1,11)}" + } + } +})"; TEST_F(ScrollTest, Sequence) { @@ -480,20 +516,7 @@ static const char *GRID_SEQUENCE_TEST_HORIZONTAL = R"({ "item": { "type": "Frame" }, - "data": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12 - ] + "data": "${Array.range(1,13)}" } } })"; @@ -546,30 +569,25 @@ TEST_F(ScrollTest, GridSequenceRTL) ASSERT_EQ(Point(0,0), component->scrollPosition()); } -static const char *SEQUENCE_TEST_HORIZONTAL_SMALL = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"horizontal\"," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"width\": 50," - " \"height\": 100" - " }," - " \"data\": [" - " 1," - " 2," - " 3" - " ]" - " }" - " }" - "}"; +static const char *SEQUENCE_TEST_HORIZONTAL_SMALL = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "horizontal", + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "width": 50, + "height": 100 + }, + "data": [1, 2, 3] + } + } +})"; TEST_F(ScrollTest, SequenceSmall) { @@ -602,40 +620,28 @@ TEST_F(ScrollTest, SequenceSmallRTL) } -static const char *SEQUENCE_TEST_HORIZONTAL_PADDING_SPACING = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"horizontal\"," - " \"paddingLeft\": 50," - " \"paddingRight\": 50," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"spacing\": 10," - " \"width\": 100," - " \"height\": 100" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6," - " 7," - " 8," - " 9," - " 10" - " ]" - " }" - " }" - "}"; +static const char *SEQUENCE_TEST_HORIZONTAL_PADDING_SPACING = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "horizontal", + "paddingLeft": 50, + "paddingRight": 50, + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "spacing": 10, + "width": 100, + "height": 100 + }, + "data": "${Array.range(1,11)}" + } + } +})"; TEST_F(ScrollTest, SequenceHorizontalPaddingSpacing) @@ -665,37 +671,26 @@ TEST_F(ScrollTest, SequenceHorizontalPaddingSpacing) ASSERT_EQ(Point(0,0), component->scrollPosition()); } -static const char *SEQUENCE_TEST_VERTICAL = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"vertical\"," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 100" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6," - " 7," - " 8," - " 9," - " 10" - " ]" - " }" - " }" - "}"; +static const char *SEQUENCE_TEST_VERTICAL = R"( +{ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "vertical", + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "width": 100, + "height": 100 + }, + "data": "${Array.range(1,11)}" + } + } +})"; TEST_F(ScrollTest, SequenceVertical) { @@ -735,20 +730,7 @@ static const char *GRID_SEQUENCE_TEST_VERTICAL = R"({ "items": { "type": "Frame" }, - "data": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12 - ] + "data": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] } } })"; @@ -776,40 +758,28 @@ TEST_F(ScrollTest, GridSequenceVertical) ASSERT_EQ(Point(0,0), component->scrollPosition()); } -static const char *SEQUENCE_TEST_VERTICAL_PADDING_SPACING = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"vertical\"," - " \"paddingTop\": 50," - " \"paddingBottom\": 50," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"spacing\": 10," - " \"width\": 100," - " \"height\": 100" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6," - " 7," - " 8," - " 9," - " 10" - " ]" - " }" - " }" - "}"; +static const char *SEQUENCE_TEST_VERTICAL_PADDING_SPACING = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "vertical", + "paddingTop": 50, + "paddingBottom": 50, + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "spacing": 10, + "width": 100, + "height": 100 + }, + "data": "${Array.range(1,11)}" + } + } +})"; TEST_F(ScrollTest, SequenceVerticalPaddingSpacing) { @@ -838,40 +808,28 @@ TEST_F(ScrollTest, SequenceVerticalPaddingSpacing) ASSERT_EQ(Point(0,0), component->scrollPosition()); } -static const char *SEQUENCE_TEST_VERTICAL_PADDING_SPACING_SMALL = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"vertical\"," - " \"paddingTop\": 50," - " \"paddingBottom\": 50," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"spacing\": 10," - " \"width\": 100," - " \"height\": 10" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6," - " 7," - " 8," - " 9," - " 10" - " ]" - " }" - " }" - "}"; +static const char *SEQUENCE_TEST_VERTICAL_PADDING_SPACING_SMALL = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "vertical", + "paddingTop": 50, + "paddingBottom": 50, + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "spacing": 10, + "width": 100, + "height": 10 + }, + "data": "${Array.range(1,11)}" + } + } +})"; TEST_F(ScrollTest, SequenceVerticalPaddingSpacingSmall) { @@ -891,37 +849,26 @@ TEST_F(ScrollTest, SequenceVerticalPaddingSpacingSmall) ASSERT_EQ(Point(0,0), component->scrollPosition()); } -static const char *SEQUENCE_DIFFERENT_UNITS = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"vertical\"," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 200" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6," - " 7," - " 8," - " 9," - " 10" - " ]" - " }" - " }" - "}"; +static const char *SEQUENCE_DIFFERENT_UNITS = R"( +{ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "vertical", + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "width": 100, + "height": 200 + }, + "data": "${Array.range(1,11)}" + } + } +})"; TEST_F(ScrollTest, DifferentUnits) { @@ -946,20 +893,19 @@ TEST_F(ScrollTest, DifferentUnits) ASSERT_EQ(Point(0, 80), component->scrollPosition()); } -static const char *SEQUENCE_EMPTY = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": []" - " }" - " }" - "}"; +static const char *SEQUENCE_EMPTY = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "id": "foo", + "width": 200, + "height": 300, + "items": [] + } + } +})"; TEST_F(ScrollTest, SequenceEmpty) @@ -973,37 +919,26 @@ TEST_F(ScrollTest, SequenceEmpty) ASSERT_EQ(Point(0,0), component->scrollPosition()); } -static const char *SEQUENCE_WITH_INDEX = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"vertical\"," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 100" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6," - " 7," - " 8," - " 9," - " 10" - " ]" - " }" - " }" - "}"; +static const char *SEQUENCE_WITH_INDEX = R"( +{ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "vertical", + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "width": 100, + "height": 100 + }, + "data": "${Array.range(1,11)}" + } + } +})"; TEST_F(ScrollTest, ScrollToIndexFirst) { @@ -1136,40 +1071,29 @@ TEST_F(ScrollTest, ScrollToIndexVisible) } -static const char *SEQUENCE_WITH_INDEX_AND_PADDING = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"vertical\"," - " \"id\": \"foo\"," - " \"width\": 200," - " \"height\": 300," - " \"paddingTop\": 50," - " \"paddingBottom\": 50," - " \"items\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 100," - " \"spacing\": 10" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6," - " 7," - " 8," - " 9," - " 10" - " ]" - " }" - " }" - "}"; +static const char *SEQUENCE_WITH_INDEX_AND_PADDING = R"( +{ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "vertical", + "id": "foo", + "width": 200, + "height": 300, + "paddingTop": 50, + "paddingBottom": 50, + "items": { + "type": "Frame", + "width": 100, + "height": 100, + "spacing": 10 + }, + "data": "${Array.range(1,11)}" + } + } +})"; TEST_F(ScrollTest, ScrollToIndexFirstPadding) { @@ -1302,54 +1226,43 @@ TEST_F(ScrollTest, ScrollToIndexVisiblePadding) } -static const char *HORIZONTAL_SEQUENCE_WITH_INDEX_AND_PADDING = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"horizontal\"," - " \"id\": \"foo\"," - " \"width\": 400," - " \"height\": 300," - " \"paddingLeft\": 50," - " \"paddingRight\": 50," - " \"items\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 100," - " \"spacing\": 10" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6," - " 7," - " 8," - " 9," - " 10" - " ]" - " }" - " }" - "}"; - -TEST_F(ScrollTest, ScrollToIndexHorizontal) +static const char *HORIZONTAL_SEQUENCE_WITH_INDEX_AND_PADDING = R"( { - loadDocument(HORIZONTAL_SEQUENCE_WITH_INDEX_AND_PADDING); - - // The second item is already in view - scrollToIndex(component, 1, kCommandScrollAlignVisible); - ASSERT_EQ(Point(0,0), component->scrollPosition()); - - // Force it to the left - scrollToIndex(component, 1, kCommandScrollAlignFirst); - ASSERT_EQ(Point(110,0), component->scrollPosition()); - - // Center (center of child=600, center of view=150) + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "horizontal", + "id": "foo", + "width": 400, + "height": 300, + "paddingLeft": 50, + "paddingRight": 50, + "items": { + "type": "Frame", + "width": 100, + "height": 100, + "spacing": 10 + }, + "data": "${Array.range(1,11)}" + } + } +})"; + +TEST_F(ScrollTest, ScrollToIndexHorizontal) +{ + loadDocument(HORIZONTAL_SEQUENCE_WITH_INDEX_AND_PADDING); + + // The second item is already in view + scrollToIndex(component, 1, kCommandScrollAlignVisible); + ASSERT_EQ(Point(0,0), component->scrollPosition()); + + // Force it to the left + scrollToIndex(component, 1, kCommandScrollAlignFirst); + ASSERT_EQ(Point(110,0), component->scrollPosition()); + + // Center (center of child=600, center of view=150) scrollToIndex(component, 5, kCommandScrollAlignCenter); ASSERT_EQ(Point(450, 0), component->scrollPosition()); @@ -1377,41 +1290,28 @@ TEST_F(ScrollTest, ScrollToIndexHorizontal) ASSERT_EQ(Point(450,0), component->scrollPosition()); } -static const char *HORIZONTAL_SEQUENCE_WITH_INDEX_RTL = R"( - { - "type": "APL", - "version": "1.7", - "mainTemplate": { - "items": { - "type": "Sequence", - "scrollDirection": "horizontal", - "layoutDirection": "RTL", - "id": "foo", - "width": 400, - "height": 300, - "paddingLeft": 50, - "paddingRight": 50, - "items": { - "type": "Frame", - "width": 100, - "height": 100 - }, - "data": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10 - ] - } - } +static const char *HORIZONTAL_SEQUENCE_WITH_INDEX_RTL = R"({ + "type": "APL", + "version": "1.7", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "horizontal", + "layoutDirection": "RTL", + "id": "foo", + "width": 400, + "height": 300, + "paddingLeft": 50, + "paddingRight": 50, + "items": { + "type": "Frame", + "width": 100, + "height": 100 + }, + "data": "${Array.range(1,11)}" } -)"; + } +})"; TEST_F(ScrollTest, ScrollToIndexHorizontalRTL) { @@ -1446,34 +1346,28 @@ TEST_F(ScrollTest, ScrollToIndexHorizontalRTL) } -static const char *MISSING_INDEX = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"scrollDirection\": \"horizontal\"," - " \"id\": \"foo\"," - " \"width\": 400," - " \"height\": 300," - " \"paddingLeft\": 50," - " \"paddingRight\": 50," - " \"items\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 100," - " \"spacing\": 10" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4" - " ]" - " }" - " }" - "}"; +static const char *MISSING_INDEX = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "horizontal", + "id": "foo", + "width": 400, + "height": 300, + "paddingLeft": 50, + "paddingRight": 50, + "items": { + "type": "Frame", + "width": 100, + "height": 100, + "spacing": 10 + }, + "data": "${Array.range(1,5)}" + } + } +})"; TEST_F(ScrollTest, ScrollToMissingIndex) { @@ -1506,62 +1400,61 @@ TEST_F(ScrollTest, ScrollToMissingIndex) } -static const char *VERTICAL_SCROLLVIEW = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"ScrollView\"," - " \"paddingTop\": 50," - " \"paddingBottom\": 50," - " \"width\": 200," - " \"height\": 300," - " \"items\": {" - " \"type\": \"Container\"," - " \"direction\": \"vertical\"," - " \"items\": [" - " {" - " \"type\": \"Frame\"," - " \"id\": \"frame1\"," - " \"width\": 100," - " \"height\": 200" - " }," - " {" - " \"type\": \"Frame\"," - " \"id\": \"frame2\"," - " \"width\": 100," - " \"height\": 300" - " }," - " {" - " \"type\": \"Frame\"," - " \"id\": \"frame3\"," - " \"width\": 100," - " \"height\": 100" - " }," - " {" - " \"type\": \"Frame\"," - " \"id\": \"frame4\"," - " \"width\": 100," - " \"height\": 400" - " }," - " {" - " \"type\": \"Frame\"," - " \"id\": \"frame5\"," - " \"width\": 100," - " \"height\": 100" - " }," - " {" - " \"type\": \"Frame\"," - " \"id\": \"frame6\"," - " \"width\": 100," - " \"height\": 300" - " }" - " ]" - " }" - " }" - " }" - "}"; +static const char *VERTICAL_SCROLLVIEW = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "ScrollView", + "paddingTop": 50, + "paddingBottom": 50, + "width": 200, + "height": 300, + "items": { + "type": "Container", + "direction": "vertical", + "items": [ + { + "type": "Frame", + "id": "frame1", + "width": 100, + "height": 200 + }, + { + "type": "Frame", + "id": "frame2", + "width": 100, + "height": 300 + }, + { + "type": "Frame", + "id": "frame3", + "width": 100, + "height": 100 + }, + { + "type": "Frame", + "id": "frame4", + "width": 100, + "height": 400 + }, + { + "type": "Frame", + "id": "frame5", + "width": 100, + "height": 100 + }, + { + "type": "Frame", + "id": "frame6", + "width": 100, + "height": 300 + } + ] + } + } + } +})"; TEST_F(ScrollTest, ScrollToComponent) { @@ -1643,38 +1536,38 @@ TEST_F(ScrollTest, ScrollWithTermination) ASSERT_EQ(currentPosition, component->scrollPosition()); } -static const char *VERTICAL_DEEP_SEQUENCE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"id\": \"seq\"," - " \"width\": 600," - " \"height\": 600," - " \"data\": [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]," - " \"scrollDirection\": \"vertical\"," - " \"items\": {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"item${data}\"," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"container${data}\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text${data}\"," - " \"width\": 150," - " \"height\": 150," - " \"text\": \"${data}\"" - " }" - " ]" - " }" - " }" - " }" - " }" - "}"; +static const char *VERTICAL_DEEP_SEQUENCE = R"( +{ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "id": "seq", + "width": 600, + "height": 600, + "data": "${Array.range(0, 20)}", + "scrollDirection": "vertical", + "items": { + "type": "TouchWrapper", + "id": "item${data}", + "item": { + "type": "Container", + "id": "container${data}", + "items": [ + { + "type": "Text", + "id": "text${data}", + "width": 150, + "height": 150, + "text": "${data}" + } + ] + } + } + } + } +})"; TEST_F(ScrollTest, SequenceToVerticalComponent) { @@ -1717,34 +1610,34 @@ TEST_F(ScrollTest, SequenceToVerticalSubComponent) session->checkAndClear(); } -static const char *HORIZONTAL_DEEP_SEQUENCE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"id\": \"seq\"," - " \"width\": 600," - " \"height\": 500," - " \"data\": [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]," - " \"scrollDirection\": \"horizontal\"," - " \"items\": {" - " \"type\": \"Container\"," - " \"id\": \"item${index}\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text${data}\"," - " \"width\": 150," - " \"height\": 150," - " \"text\": \"${data}\"" - " }" - " ]" - " }" - " }" - " }" - "}"; +static const char *HORIZONTAL_DEEP_SEQUENCE = R"( +{ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "id": "seq", + "width": 600, + "height": 500, + "data": "${Array.range(0, 20)}", + "scrollDirection": "horizontal", + "items": { + "type": "Container", + "id": "item${index}", + "items": [ + { + "type": "Text", + "id": "text${data}", + "width": 150, + "height": 150, + "text": "${data}" + } + ] + } + } + } +})"; TEST_F(ScrollTest, SequenceToHorizontalComponent) { @@ -1832,38 +1725,32 @@ TEST_F(ScrollTest, SequenceToHorizontalSubComponentRTL) session->checkAndClear(); } -static const char *PAGER_TEST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.6\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Pager\"," - " \"id\": \"myPager\"," - " \"width\": 100," - " \"height\": 100," - " \"items\": {" - " \"type\": \"Text\"," - " \"id\": \"id${data}\"," - " \"text\": \"TEXT${data}\"," - " \"speech\": \"URL${data}\"" - " }," - " \"data\": [" - " 1," - " 2," - " 3," - " 4" - " ]," - " \"onPageChanged\": {" - " \"type\": \"SendEvent\"," - " \"sequencer\": \"SET_PAGE\"," - " \"arguments\": [" - " \"${event.target.page}\"" - " ]" - " }" - " }" - " }" - "}"; +static const char *PAGER_TEST = R"({ + "type": "APL", + "version": "1.6", + "mainTemplate": { + "items": { + "type": "Pager", + "id": "myPager", + "width": 100, + "height": 100, + "items": { + "type": "Text", + "id": "id${data}", + "text": "TEXT${data}", + "speech": "URL${data}" + }, + "data": "${Array.range(1, 5)}", + "onPageChanged": { + "type": "SendEvent", + "sequencer": "SET_PAGE", + "arguments": [ + "${event.target.page}" + ] + } + } + } +})"; TEST_F(ScrollTest, Pager) { @@ -1877,87 +1764,85 @@ TEST_F(ScrollTest, Pager) } -static const char *TEST_BASIC_TOP_BOTTOM_OFFSET_STICKY = R"apl( -{ - "type": "APL", - "version": "1.6", - "mainTemplate": { - "item": [ - { - "type": "Frame", - "height": 600, - "width": 500, - "padding": 40, - "backgroundColor": "black", - "items": [ - { - "id": "scrollone", - "type": "ScrollView", - "width": 400, - "height": 500, - "item" : { - "type": "Container", - "height": 1000, - "width": 400, - "items": [ - { - "type": "Frame", - "height": 400, - "width": 200, - "backgroundColor": "white", - "items": [] - }, - { - "type": "Frame", - "height": 300, - "width": 400, - "backgroundColor": "#1a73e8", - "items": [ - { - "type": "Container", - "height": 300, - "width": 400, - "items": [ - { - "id": "topsticky", - "position": "sticky", - "top": 0, - "type": "Frame", - "height": 100, - "width": 300, - "backgroundColor": "#dc3912", - "items": [] - }, - { - "type": "Frame", - "height": 100, - "width": 200, - "backgroundColor": "#4caf50", - "items": [] - }, - { - "id": "bottomsticky", - "position": "sticky", - "bottom": 0, - "type": "Frame", - "height": 100, - "width": 150, - "backgroundColor": "blue", - "items": [] - } - ] - } - ] - } - ] - } - } - ] - } - ] - } -} -)apl"; +static const char *TEST_BASIC_TOP_BOTTOM_OFFSET_STICKY = R"apl({ + "type": "APL", + "version": "1.6", + "mainTemplate": { + "item": [ + { + "type": "Frame", + "height": 600, + "width": 500, + "padding": 40, + "backgroundColor": "black", + "items": [ + { + "id": "scrollone", + "type": "ScrollView", + "width": 400, + "height": 500, + "item": { + "type": "Container", + "height": 1000, + "width": 400, + "items": [ + { + "type": "Frame", + "height": 400, + "width": 200, + "backgroundColor": "white", + "items": [] + }, + { + "type": "Frame", + "height": 300, + "width": 400, + "backgroundColor": "#1a73e8", + "items": [ + { + "type": "Container", + "height": 300, + "width": 400, + "items": [ + { + "id": "topsticky", + "position": "sticky", + "top": 0, + "type": "Frame", + "height": 100, + "width": 300, + "backgroundColor": "#dc3912", + "items": [] + }, + { + "type": "Frame", + "height": 100, + "width": 200, + "backgroundColor": "#4caf50", + "items": [] + }, + { + "id": "bottomsticky", + "position": "sticky", + "bottom": 0, + "type": "Frame", + "height": 100, + "width": 150, + "backgroundColor": "blue", + "items": [] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } +})apl"; TEST_F(ScrollTest, BasicStickyTestTopOffset) { @@ -2020,85 +1905,83 @@ TEST_F(ScrollTest, BasicStickyTestBottomOffset) EXPECT_TRUE(expectBounds(stickyComp, 200, 0, 300, 150)); } -static const char *TEST_SKIP_BOTTOM_OFFSET_STICKY = R"apl( -{ - "type": "APL", - "version": "1.6", - "mainTemplate": { - "item": [ - { - "type": "Frame", - "height": 600, - "width": 500, - "padding": 40, - "backgroundColor": "black", - "items": [ - { - "id": "scrollone", - "type": "ScrollView", - "width": 400, - "height": 500, - "item" : { - "type": "Container", - "height": 1000, - "width": 400, - "items": [ - { - "type": "Frame", - "height": 400, - "width": 200, - "backgroundColor": "white", - "items": [] - }, - { - "type": "Frame", - "height": 800, - "width": 400, - "backgroundColor": "#1a73e8", - "items": [ - { - "type": "Container", - "height": 800, - "width": 400, - "items": [ - { - "type": "Frame", - "height": 100, - "width": 300, - "backgroundColor": "#dc3912", - "items": [] - }, - { - "type": "Frame", - "height": 100, - "width": 200, - "backgroundColor": "#4caf50", - "items": [] - }, - { - "id": "bottomsticky", - "position": "sticky", - "top": 300, - "bottom": 300, - "type": "Frame", - "height": 100, - "width": 150, - "backgroundColor": "blue", - "items": [] - } - ] - } - ] - } - ] - } - } - ] - } - ] - } -} -)apl"; +static const char *TEST_SKIP_BOTTOM_OFFSET_STICKY = R"apl({ + "type": "APL", + "version": "1.6", + "mainTemplate": { + "item": [ + { + "type": "Frame", + "height": 600, + "width": 500, + "padding": 40, + "backgroundColor": "black", + "items": [ + { + "id": "scrollone", + "type": "ScrollView", + "width": 400, + "height": 500, + "item": { + "type": "Container", + "height": 1000, + "width": 400, + "items": [ + { + "type": "Frame", + "height": 400, + "width": 200, + "backgroundColor": "white", + "items": [] + }, + { + "type": "Frame", + "height": 800, + "width": 400, + "backgroundColor": "#1a73e8", + "items": [ + { + "type": "Container", + "height": 800, + "width": 400, + "items": [ + { + "type": "Frame", + "height": 100, + "width": 300, + "backgroundColor": "#dc3912", + "items": [] + }, + { + "type": "Frame", + "height": 100, + "width": 200, + "backgroundColor": "#4caf50", + "items": [] + }, + { + "id": "bottomsticky", + "position": "sticky", + "top": 300, + "bottom": 300, + "type": "Frame", + "height": 100, + "width": 150, + "backgroundColor": "blue", + "items": [] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } +})apl"; /** * Make sure we skip the bottom offset when top + bottom offset is bigger than the scrollable height @@ -2133,135 +2016,133 @@ TEST_F(ScrollTest, BasicStickyTestSkipBottomOffset) } -static const char *TEST_TOP_NESTED_STICKY = R"apl( -{ - "type": "APL", - "version": "1.6", - "mainTemplate": { - "item": [ - { - "type": "Frame", - "height": 600, - "width": 500, - "padding": 40, - "backgroundColor": "black", - "items": [ - { - "id": "scrollone", - "type": "ScrollView", - "width": 400, - "height": 500, - "item" : { - "type": "Container", - "height": 1000, - "width": 400, - "items": [ - { - "type": "Frame", - "height": 300, - "width": 400, - "backgroundColor": "#1a73e8", - "items": [ - { - "type": "Container", - "height": 300, - "width": 400, - "items": [ - { - "position": "sticky", - "top": 0, - "bottom": 10, - "type": "Frame", - "height": 100, - "width": 300, - "backgroundColor": "#dc3912", - "items": [] - }, - { - "position": "sticky", - "top": 10, - "type": "Frame", - "height": 100, - "width": 200, - "backgroundColor": "#4caf50", - "items": [] - }, - { - "type": "Frame", - "height": 100, - "width": 150, - "backgroundColor": "blue", - "items": [] - } - ] - } - ] - }, - { - "type": "Frame", - "height": 100, - "width": 400, - "backgroundColor": "white" - }, - { - "type": "Frame", - "height": 1000, - "width": 400, - "backgroundColor": "#1a73e8", - "items": [ - { - "type": "Container", - "height": 1000, - "width": 400, - "items": [ - { - "type": "Frame", - "height": 100, - "width": 400, - "backgroundColor": "#dc3912", - "items": [] - }, - { - "position": "sticky", - "id": "outerSticky", - "top": 100, - "type": "Frame", - "height": 300, - "width": 400, - "backgroundColor": "#de7700", - "items": [ - { - "type": "Container", - "height": 300, - "width": 300, - "items": [ - { - "type": "Frame", - "id": "innnerSticky", - "position": "sticky", - "top": 120, - "height": 100, - "width": 300, - "backgroundColor": "white", - "items": [] - } - ] - } - ] - } - ] - } - ] - } - ] - } - } - ] - } - ] - } -} -)apl"; +static const char *TEST_TOP_NESTED_STICKY = R"apl({ + "type": "APL", + "version": "1.6", + "mainTemplate": { + "item": [ + { + "type": "Frame", + "height": 600, + "width": 500, + "padding": 40, + "backgroundColor": "black", + "items": [ + { + "id": "scrollone", + "type": "ScrollView", + "width": 400, + "height": 500, + "item": { + "type": "Container", + "height": 1000, + "width": 400, + "items": [ + { + "type": "Frame", + "height": 300, + "width": 400, + "backgroundColor": "#1a73e8", + "items": [ + { + "type": "Container", + "height": 300, + "width": 400, + "items": [ + { + "position": "sticky", + "top": 0, + "bottom": 10, + "type": "Frame", + "height": 100, + "width": 300, + "backgroundColor": "#dc3912", + "items": [] + }, + { + "position": "sticky", + "top": 10, + "type": "Frame", + "height": 100, + "width": 200, + "backgroundColor": "#4caf50", + "items": [] + }, + { + "type": "Frame", + "height": 100, + "width": 150, + "backgroundColor": "blue", + "items": [] + } + ] + } + ] + }, + { + "type": "Frame", + "height": 100, + "width": 400, + "backgroundColor": "white" + }, + { + "type": "Frame", + "height": 1000, + "width": 400, + "backgroundColor": "#1a73e8", + "items": [ + { + "type": "Container", + "height": 1000, + "width": 400, + "items": [ + { + "type": "Frame", + "height": 100, + "width": 400, + "backgroundColor": "#dc3912", + "items": [] + }, + { + "position": "sticky", + "id": "outerSticky", + "top": 100, + "type": "Frame", + "height": 300, + "width": 400, + "backgroundColor": "#de7700", + "items": [ + { + "type": "Container", + "height": 300, + "width": 300, + "items": [ + { + "type": "Frame", + "id": "innnerSticky", + "position": "sticky", + "top": 120, + "height": 100, + "width": 300, + "backgroundColor": "white", + "items": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } +})apl"; TEST_F(ScrollTest, NestedStickyComponents) { @@ -2297,8 +2178,7 @@ TEST_F(ScrollTest, NestedStickyComponents) EXPECT_TRUE(expectBounds(stickyCompInner, 20, 0, 120, 300)); } -static const char *DEEP_NESTED_COMPONENTS = R"apl( -{ +static const char *DEEP_NESTED_COMPONENTS = R"apl({ "type": "APL", "version": "1.6", "mainTemplate": { @@ -2430,8 +2310,7 @@ static const char *DEEP_NESTED_COMPONENTS = R"apl( } ] } -} -)apl"; +})apl"; TEST_F(ScrollTest, DeepNestedStickyComponents) { @@ -2507,90 +2386,88 @@ TEST_F(ScrollTest, DeepNestedStickyComponents) EXPECT_TRUE(expectBounds(stickyCompInner4, 0, 0, 70, 270)); } -static const char *TEST_BASIC_LEFT_RIGHT_OFFSET_STICKY = R"apl( -{ - "type": "APL", - "version": "1.6", - "mainTemplate": { - "item": [ - { - "type": "Frame", - "height": 600, - "width": 500, - "padding": 40, - "backgroundColor": "black", - "items": [ - { - "id": "scrollone", - "type": "Sequence", - "scrollDirection": "horizontal", - "width": 400, - "height": 400, - "item" : { - "type": "Container", - "height": 400, - "width": 1000, - "direction": "row", - "items": [ - { - "type": "Frame", - "height": 300, - "width": 300, - "backgroundColor": "white", - "items": [] - }, - { - "type": "Frame", - "height": 300, - "width": 400, - "backgroundColor": "#1a73e8", - "items": [ - { - "type": "Container", - "height": 300, - "width": 400, - "direction": "row", - "items": [ - { - "id": "leftsticky", - "position": "sticky", - "left": 0, - "type": "Frame", - "height": 300, - "width": 100, - "backgroundColor": "#dc3912", - "items": [] - }, - { - "type": "Frame", - "height": 200, - "width": 100, - "backgroundColor": "#4caf50", - "items": [] - }, - { - "id": "rightsticky", - "position": "sticky", - "right": 0, - "type": "Frame", - "height": 150, - "width": 100, - "backgroundColor": "blue", - "items": [] - } - ] - } - ] - } - ] - } - } - ] - } - ] - } -} -)apl"; +static const char *TEST_BASIC_LEFT_RIGHT_OFFSET_STICKY = R"apl({ + "type": "APL", + "version": "1.6", + "mainTemplate": { + "item": [ + { + "type": "Frame", + "height": 600, + "width": 500, + "padding": 40, + "backgroundColor": "black", + "items": [ + { + "id": "scrollone", + "type": "Sequence", + "scrollDirection": "horizontal", + "width": 400, + "height": 400, + "item": { + "type": "Container", + "height": 400, + "width": 1000, + "direction": "row", + "items": [ + { + "type": "Frame", + "height": 300, + "width": 300, + "backgroundColor": "white", + "items": [] + }, + { + "type": "Frame", + "height": 300, + "width": 400, + "backgroundColor": "#1a73e8", + "items": [ + { + "type": "Container", + "height": 300, + "width": 400, + "direction": "row", + "items": [ + { + "id": "leftsticky", + "position": "sticky", + "left": 0, + "type": "Frame", + "height": 300, + "width": 100, + "backgroundColor": "#dc3912", + "items": [] + }, + { + "type": "Frame", + "height": 200, + "width": 100, + "backgroundColor": "#4caf50", + "items": [] + }, + { + "id": "rightsticky", + "position": "sticky", + "right": 0, + "type": "Frame", + "height": 150, + "width": 100, + "backgroundColor": "blue", + "items": [] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } +})apl"; TEST_F(ScrollTest, BasicStickyTestLeftOffset) { @@ -2687,8 +2564,7 @@ TEST_F(ScrollTest, StickyTestSkipRightOffset) EXPECT_TRUE(expectBounds(stickyComp, 0, 300, 150, 400)); } -static const char *TEST_LEFT_NESTED_STICKY = R"apl( -{ +static const char *TEST_LEFT_NESTED_STICKY = R"apl({ "type": "APL", "version": "1.6", "mainTemplate": { @@ -2793,8 +2669,7 @@ static const char *TEST_LEFT_NESTED_STICKY = R"apl( }] }] } -} -)apl"; +})apl"; TEST_F(ScrollTest, NestedStickyComponentsLeft) { @@ -2861,8 +2736,7 @@ TEST_F(ScrollTest, DeepNestedStickyComponentsAddRemove) EXPECT_TRUE(expectBounds(leafSticky, 0, 10, 300, 110)); } -static const char *TEST_BASIC_TOP_BOTTOM_OFFSET_STICKY_WITHOUT_STICKIES = R"apl( -{ +static const char *TEST_BASIC_TOP_BOTTOM_OFFSET_STICKY_WITHOUT_STICKIES = R"apl({ "type": "APL", "version": "1.6", "mainTemplate": { @@ -2929,34 +2803,29 @@ static const char *TEST_BASIC_TOP_BOTTOM_OFFSET_STICKY_WITHOUT_STICKIES = R"apl( } ] } -} -)apl"; - -const static char * STICKY_CHILD_TOP = R"apl( - { - "id": "topsticky", - "position": "sticky", - "top": 0, - "type": "Frame", - "height": 100, - "width": 300, - "backgroundColor": "#dc3912", - "items": [] - } -)apl"; - -const static char * STICKY_CHILD_BOTTOM = R"apl( - { - "id": "bottomsticky", - "position": "sticky", - "bottom": 0, - "type": "Frame", - "height": 100, - "width": 150, - "backgroundColor": "blue", - "items": [] - } -)apl"; +})apl"; + +const static char * STICKY_CHILD_TOP = R"apl({ + "id": "topsticky", + "position": "sticky", + "top": 0, + "type": "Frame", + "height": 100, + "width": 300, + "backgroundColor": "#dc3912", + "items": [] +})apl"; + +const static char * STICKY_CHILD_BOTTOM = R"apl({ + "id": "bottomsticky", + "position": "sticky", + "bottom": 0, + "type": "Frame", + "height": 100, + "width": 150, + "backgroundColor": "blue", + "items": [] +})apl"; /** * Check if an inserted child registers it's scroll callback correctly @@ -3011,26 +2880,24 @@ TEST_F(ScrollTest, InsertStickyChildTest) EXPECT_TRUE(expectBounds(stickyBottom, 150, 0, 250, 150)); } -const static char * NON_STICKY_CHILD_TOP = R"apl( +const static char * NON_STICKY_CHILD_TOP = R"apl({ + "type": "Frame", + "height": 100, + "width": 300, + "backgroundColor": "#dc3912", + "items": [ { - "type": "Frame", - "height": 100, - "width": 300, - "backgroundColor": "#dc3912", - "items": [ - { - "id": "topsticky", - "position": "sticky", - "top": 0, - "type": "Frame", - "height": 100, - "width": 300, - "backgroundColor": "black", - "items": [] - } - ] + "id": "topsticky", + "position": "sticky", + "top": 0, + "type": "Frame", + "height": 100, + "width": 300, + "backgroundColor": "black", + "items": [] } -)apl"; + ] +})apl"; /** * Check inserting child which isn't sticky but contains a sticky child @@ -3066,8 +2933,7 @@ TEST_F(ScrollTest, InsertStickyChildComplexTest) advanceTime(200); } -const static char * SCROLLABLE_WITH_STICKY = R"apl( -{ +const static char * SCROLLABLE_WITH_STICKY = R"apl({ "id": "scrollableWithStickyChild", "type": "ScrollView", "height": 300, @@ -3093,8 +2959,7 @@ const static char * SCROLLABLE_WITH_STICKY = R"apl( ] } ] -} -)apl"; +})apl"; /** * Check inserting child which is scrollable and contains a sticky child @@ -3127,28 +2992,24 @@ TEST_F(ScrollTest, InsertScrollableWithStickyChildTest) EXPECT_TRUE(expectBounds(stickyTop, 0, 0, 100, 300)); } -const static char * NON_STICKY_CHILD_TOP_WITH_OFFSET = R"apl( - { - "id": "topsticky", - "top": 100, - "type": "Frame", - "height": 100, - "width": 300, - "backgroundColor": "#dc3912", - "items": [] - } -)apl"; - -const static char * NON_STICKY_CHILD_BOTTOM_WITHOUT_OFFSET = R"apl( - { - "id": "bottomsticky", - "type": "Frame", - "height": 100, - "width": 150, - "backgroundColor": "blue", - "items": [] - } -)apl"; +const static char * NON_STICKY_CHILD_TOP_WITH_OFFSET = R"apl({ + "id": "topsticky", + "top": 100, + "type": "Frame", + "height": 100, + "width": 300, + "backgroundColor": "#dc3912", + "items": [] +})apl"; + +const static char * NON_STICKY_CHILD_BOTTOM_WITHOUT_OFFSET = R"apl({ + "id": "bottomsticky", + "type": "Frame", + "height": 100, + "width": 150, + "backgroundColor": "blue", + "items": [] +})apl"; TEST_F(ScrollTest, SetUnsetStickyChildTest) { @@ -3228,8 +3089,7 @@ TEST_F(ScrollTest, SetUnsetStickyChildTest) EXPECT_TRUE(expectBounds(stickyBottom, 150, 0, 250, 150)); } -const static char * NESTED_SCROLLABLES_WITH_STICKY = R"apl( -{ +const static char * NESTED_SCROLLABLES_WITH_STICKY = R"apl({ "type": "APL", "version": "1.6", "mainTemplate": { @@ -3375,8 +3235,7 @@ const static char * NESTED_SCROLLABLES_WITH_STICKY = R"apl( } ] } -} -)apl"; +})apl"; /** * Make sure a sticky components in a nested scrollable don't react to a scrollable ancestor @@ -3417,8 +3276,7 @@ TEST_F(ScrollTest, NestedScrollablesSameTypeWithStickies) /** * Make sure a combination of horizontal and vertical scrollables works */ -const static char * NESTED_SCROLLABLES_WITH_SAME_AND_DIFFERENT_TYPES = R"apl( - { +const static char * NESTED_SCROLLABLES_WITH_SAME_AND_DIFFERENT_TYPES = R"apl({ "type": "APL", "version": "1.6", "mainTemplate": { @@ -3514,8 +3372,7 @@ const static char * NESTED_SCROLLABLES_WITH_SAME_AND_DIFFERENT_TYPES = R"apl( } ] } -} -)apl"; +})apl"; TEST_F(ScrollTest, NestedScrollablesSameAndDifferntTypeWithStickies) { @@ -3583,8 +3440,7 @@ TEST_F(ScrollTest, NestedScrollablesSameAndDifferntTypeWithStickies) EXPECT_TRUE(expectBounds(deepestSticky, 420, 500, 570, 650)); } -const static char * REMOVE_STICKY_COMPONENT_DOC = R"apl( -{ +const static char * REMOVE_STICKY_COMPONENT_DOC = R"apl({ "type": "APL", "version": "1.6", "mainTemplate": { @@ -3608,8 +3464,7 @@ const static char * REMOVE_STICKY_COMPONENT_DOC = R"apl( } } } -} -)apl"; +})apl"; /** * Make sure a removed component doesn't react to scrolling @@ -3649,8 +3504,7 @@ TEST_F(ScrollTest, RemoveAndReplaceStickyComponet) } -const static char * REPLACE_STICKY_COMPONENT_DOC = R"apl( -{ +const static char * REPLACE_STICKY_COMPONENT_DOC = R"apl({ "type": "APL", "version": "1.6", "mainTemplate": { @@ -3690,8 +3544,7 @@ const static char * REPLACE_STICKY_COMPONENT_DOC = R"apl( ] } } -} -)apl"; +})apl"; /** * Move a component from one scrollable to another and check offsets are correct @@ -3743,3 +3596,176 @@ TEST_F(ScrollTest, ReplaceAndCheckStickyComponent) EXPECT_TRUE(expectBounds(stickyTop, 200, 0, 300, 100)); } +static const char *SEQUENCE_TEST_VERTICAL_DURATION = R"( +{ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "vertical", + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "width": 100, + "height": 100 + }, + "data": "${Array.range(1,11)}" + } + } +})"; + +TEST_F(ScrollTest, SequenceVerticalDuration) +{ + loadDocument(SEQUENCE_TEST_VERTICAL_DURATION); + + completeScroll(component, -1, 200); // Can't scroll backwards + ASSERT_EQ(Point(0,0), component->scrollPosition()); + + completeScroll(component, 1, 200); + ASSERT_EQ(Point(0, 300), component->scrollPosition()); + + completeScroll(component, 5, 200); // This maxes out + ASSERT_EQ(Point(0, 700), component->scrollPosition()); + + completeScroll(component, 5, 200); + ASSERT_EQ(Point(0, 700), component->scrollPosition()); + + completeScroll(component, -0.5f, 200); + ASSERT_EQ(Point(0, 550), component->scrollPosition()); + + completeScroll(component, -20, 200); + ASSERT_EQ(Point(0,0), component->scrollPosition()); +} + +static const char *SEQUENCE_WITH_INDEX_DURATION = R"( +{ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "items": { + "type": "Sequence", + "scrollDirection": "vertical", + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "Frame", + "width": 100, + "height": 100 + }, + "data": "${Array.range(1,11)}" + } + } +})"; + +TEST_F(ScrollTest, ScrollToIndexFirstDuration) +{ + loadDocument(SEQUENCE_WITH_INDEX_DURATION); + + // Move the second item up to the top of the scroll view. + scrollToIndex(component, 1, kCommandScrollAlignFirst, 200); + ASSERT_EQ(Point(0,100), component->scrollPosition()); + + // Repeat the command - it shouldn't move. + scrollToIndex(component, 1, kCommandScrollAlignFirst, 200); + ASSERT_EQ(Point(0,100), component->scrollPosition()); + + scrollToIndex(component, 5, kCommandScrollAlignFirst, 200); + ASSERT_EQ(Point(0,500), component->scrollPosition()); + + scrollToIndex(component, 3, kCommandScrollAlignFirst, 200); + ASSERT_EQ(Point(0,300), component->scrollPosition()); + + // The last component can't scroll all the way to the top + scrollToIndex(component, 9, kCommandScrollAlignFirst, 200); + ASSERT_EQ(Point(0, 700), component->scrollPosition()); + + scrollToIndex(component, 0, kCommandScrollAlignFirst, 200); + ASSERT_EQ(Point(0,0), component->scrollPosition()); + + scrollToIndex(component, -5, kCommandScrollAlignFirst, 200); + ASSERT_EQ(Point(0,500), component->scrollPosition()); +} + +TEST_F(ScrollTest, ScrollToComponentDuration) +{ + loadDocument(VERTICAL_SCROLLVIEW); + + std::map map; + for (int i = 1 ; i <= 6 ; i++) { + std::string name = "frame" + std::to_string(i); + map.emplace(name, root->context().findComponentById(name)); + } + + scrollToComponent(map["frame4"], kCommandScrollAlignFirst, 300); + ASSERT_EQ(Point(0,600), component->scrollPosition()); +} + +TEST_F(ScrollTest, SequenceVerticalInstant) +{ + loadDocument(SEQUENCE_TEST_VERTICAL_DURATION); + + completeScroll(component, -1, 0); // Can't scroll backwards + ASSERT_EQ(Point(0,0), component->scrollPosition()); + + completeScroll(component, 1, 0); + ASSERT_EQ(Point(0, 300), component->scrollPosition()); + + completeScroll(component, 5, 0); // This maxes out + ASSERT_EQ(Point(0, 700), component->scrollPosition()); + + completeScroll(component, 5, 0); + ASSERT_EQ(Point(0, 700), component->scrollPosition()); + + completeScroll(component, -0.5f, 0); + ASSERT_EQ(Point(0, 550), component->scrollPosition()); + + completeScroll(component, -20, 0); + ASSERT_EQ(Point(0,0), component->scrollPosition()); +} + +TEST_F(ScrollTest, ScrollToIndexFirstInstant) +{ + loadDocument(SEQUENCE_WITH_INDEX_DURATION); + + // Move the second item up to the top of the scroll view. + scrollToIndex(component, 1, kCommandScrollAlignFirst, 0); + ASSERT_EQ(Point(0,100), component->scrollPosition()); + + // Repeat the command - it shouldn't move. + scrollToIndex(component, 1, kCommandScrollAlignFirst, 0); + ASSERT_EQ(Point(0,100), component->scrollPosition()); + + scrollToIndex(component, 5, kCommandScrollAlignFirst, 0); + ASSERT_EQ(Point(0,500), component->scrollPosition()); + + scrollToIndex(component, 3, kCommandScrollAlignFirst, 0); + ASSERT_EQ(Point(0,300), component->scrollPosition()); + + // The last component can't scroll all the way to the top + scrollToIndex(component, 9, kCommandScrollAlignFirst, 0); + ASSERT_EQ(Point(0, 700), component->scrollPosition()); + + scrollToIndex(component, 0, kCommandScrollAlignFirst, 0); + ASSERT_EQ(Point(0,0), component->scrollPosition()); + + scrollToIndex(component, -5, kCommandScrollAlignFirst, 0); + ASSERT_EQ(Point(0,500), component->scrollPosition()); +} + +TEST_F(ScrollTest, ScrollToComponentInstant) +{ + loadDocument(VERTICAL_SCROLLVIEW); + + std::map map; + for (int i = 1 ; i <= 6 ; i++) { + std::string name = "frame" + std::to_string(i); + map.emplace(name, root->context().findComponentById(name)); + } + + scrollToComponent(map["frame4"], kCommandScrollAlignFirst, 0); + ASSERT_EQ(Point(0,600), component->scrollPosition()); +} \ No newline at end of file diff --git a/aplcore/unit/component/unittest_serialize.cpp b/aplcore/unit/component/unittest_serialize.cpp index e31e378..ac14a51 100644 --- a/aplcore/unit/component/unittest_serialize.cpp +++ b/aplcore/unit/component/unittest_serialize.cpp @@ -269,10 +269,10 @@ TEST_F(SerializeTest, Components) ASSERT_EQ(frame->getCalculated(kPropertyBorderColor).getColor(), Color(session, frameJson["borderColor"].GetString())); ASSERT_EQ(frame->getCalculated(kPropertyBorderWidth).getAbsoluteDimension(), frameJson["borderWidth"].GetDouble()); auto action = frame->getCalculated(kPropertyAccessibilityActions).at(0).get(); - ASSERT_EQ(action->getName(), frameJson["action"][0]["name"].GetString()); - ASSERT_EQ(action->getLabel(), frameJson["action"][0]["label"].GetString()); - ASSERT_EQ(action->enabled(), frameJson["action"][0]["enabled"].GetBool()); - ASSERT_FALSE(frameJson["action"][0].HasMember("commands")); // Commands don't get serialized + ASSERT_EQ(action->getName(), frameJson["_actions"][0]["name"].GetString()); + ASSERT_EQ(action->getLabel(), frameJson["_actions"][0]["label"].GetString()); + ASSERT_EQ(action->enabled(), frameJson["_actions"][0]["enabled"].GetBool()); + ASSERT_FALSE(frameJson["_actions"][0].HasMember("commands")); // Commands don't get serialized auto sequence = context->findComponentById("sequence"); ASSERT_TRUE(sequence); @@ -414,8 +414,9 @@ const static char *SERIALIZE_ALL_RESULT = R"({ "__inheritParentState": false, "__style": "", "__path": "_main/layouts/MyLayout/items", - "accessibilityLabel": "", + "_actions": [], "action": [], + "accessibilityLabel": "", "_bounds": [ 0, 0, @@ -454,6 +455,7 @@ const static char *SERIALIZE_ALL_RESULT = R"({ "maxWidth": null, "minHeight": 0, "minWidth": 0, + "onChildrenChanged": [], "onMount": [], "onSpeechMark": [], "opacity": 1, @@ -1089,3 +1091,33 @@ TEST_F(SerializeTest, SerializeHeaders) { ASSERT_EQ(imageHeaders[i], serializedHeaders[i].GetString()); } } + +static const char* VIDEO_IN_CONTAINER = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "items": { + "type": "Container", + "width": 200, + "height": 200, + "items": { + "type": "Video", + "id": "VIDEO", + "width": "100%", + "height": "100%" + } + } + } +})"; + +TEST_F(SerializeTest, DisallowedVideoHasNoSerializedMediaPlayer) { + config->set(RootProperty::kDisallowVideo, true); + loadDocument(VIDEO_IN_CONTAINER); + ASSERT_TRUE(component); + + auto v = context->findComponentById("VIDEO"); + rapidjson::Document doc; + auto json = v->serialize(doc.GetAllocator()); + + ASSERT_FALSE(json.HasMember("__mediaPlayer")); +} \ No newline at end of file diff --git a/aplcore/unit/component/unittest_tick.cpp b/aplcore/unit/component/unittest_tick.cpp index f139e5e..a8a140f 100644 --- a/aplcore/unit/component/unittest_tick.cpp +++ b/aplcore/unit/component/unittest_tick.cpp @@ -478,7 +478,7 @@ TEST_F(TickTest, AdjustedFpsLimit) { TEST_F(TickTest, CantGo0) { config->set(RootProperty::kTickHandlerUpdateLimit, 0); - ASSERT_EQ(1.0, config->getTickHandlerUpdateLimit()); + ASSERT_EQ(1.0, config->getProperty(RootProperty::kTickHandlerUpdateLimit).getDouble()); loadDocument(UNLIMITED_UPDATES); advanceTime(0); diff --git a/aplcore/unit/component/unittest_video_component.cpp b/aplcore/unit/component/unittest_video_component.cpp new file mode 100644 index 0000000..4d9a04e --- /dev/null +++ b/aplcore/unit/component/unittest_video_component.cpp @@ -0,0 +1,213 @@ +/** +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ + +#include "../testeventloop.h" + +#include "../media/testmediaplayerfactory.h" +#include "apl/component/videocomponent.h" +#include "apl/focus/focusmanager.h" +#include "apl/primitives/object.h" + +using namespace apl; + +class VideoComponentTest : public DocumentWrapper { +public: + VideoComponentTest() : DocumentWrapper() { + mediaPlayerFactory = std::make_shared(); + config->mediaPlayerFactory(mediaPlayerFactory); + } + + std::shared_ptr mediaPlayerFactory; +}; + +static const char* VIDEO_IN_CONTAINER = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "items": { + "type": "Container", + "width": 200, + "height": 200, + "items": { + "type": "Video", + "id": "VIDEO", + "width": "100%", + "height": "100%" + } + } + } +})"; + +TEST_F(VideoComponentTest, DisallowVideoTrueDisallowsComponent) { + config->set(RootProperty::kDisallowVideo, true); + loadDocument(VIDEO_IN_CONTAINER); + + ASSERT_TRUE(component); + auto v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), true); + // No media player when disallow is true + ASSERT_EQ(v->getMediaPlayer(), nullptr); +} + +TEST_F(VideoComponentTest, DisallowVideoFalseAllowsComponent) { + config->set(RootProperty::kDisallowVideo, false); + loadDocument(VIDEO_IN_CONTAINER); + + ASSERT_TRUE(component); + auto v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), false); + // Has media player when disallow is false + ASSERT_TRUE(v->getMediaPlayer()); +} + +static const char* VIDEO_IN_CONTAINER_WITH_REINFLATE = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "items": { + "type": "Container", + "width": 200, + "height": 200, + "items": { + "type": "Video", + "id": "VIDEO", + "width": "100%", + "height": "100%" + } + } + } +})"; + +TEST_F(VideoComponentTest, ConfigChangeDisallowVideoTrueToFalseWillAllowComponent) { + // Initial configuration + config->set(RootProperty::kDisallowVideo, true); + loadDocument(VIDEO_IN_CONTAINER_WITH_REINFLATE); + ASSERT_TRUE(component); + + auto v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), true); + // No media player when disallow is true + ASSERT_EQ(v->getMediaPlayer(), nullptr); + + // Trigger config change + auto configChange = ConfigurationChange().disallowVideo(false); + root->configurationChange(configChange); + processReinflate(); + advanceTime(100); + + v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), false); + // Has media player when disallow is false + ASSERT_TRUE(v->getMediaPlayer()); +} + +TEST_F(VideoComponentTest, ConfigChangeDisallowVideoFalseToTrueWillDisallowComponent) { + // Initial configuration + config->set(RootProperty::kDisallowVideo, false); + loadDocument(VIDEO_IN_CONTAINER_WITH_REINFLATE); + + ASSERT_TRUE(component); + auto v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), false); + // Has media player when disallow is false + ASSERT_TRUE(v->getMediaPlayer()); + + // Trigger config change + auto configChange = ConfigurationChange().disallowVideo(true); + root->configurationChange(configChange); + processReinflate(); + advanceTime(100); + + v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), true); + // No media player when disallow is true + ASSERT_EQ(v->getMediaPlayer(), nullptr); +} + +TEST_F(VideoComponentTest, ConfigChangeDisallowVideoFalseToFalseDoesntDisallowComponent) { + // Initial configuration + config->set(RootProperty::kDisallowVideo, false); + loadDocument(VIDEO_IN_CONTAINER_WITH_REINFLATE); + + ASSERT_TRUE(component); + auto v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), false); + // Has media player when disallow is false + ASSERT_TRUE(v->getMediaPlayer()); + + // Trigger config change + auto configChange = ConfigurationChange().disallowVideo(false); + root->configurationChange(configChange); + processReinflate(); + advanceTime(100); + + v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), false); + // Has media player when disallow is false + ASSERT_TRUE(v->getMediaPlayer()); +} + +TEST_F(VideoComponentTest, ConfigChangeDisallowVideoTrueToTrueDoesntAllowComponent) { + // Initial configuration + config->set(RootProperty::kDisallowVideo, true); + loadDocument(VIDEO_IN_CONTAINER_WITH_REINFLATE); + + ASSERT_TRUE(component); + auto v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), true); + // No media player when disallow is true + ASSERT_EQ(v->getMediaPlayer(), nullptr); + + // Trigger config change + auto configChange = ConfigurationChange().disallowVideo(true); + root->configurationChange(configChange); + processReinflate(); + advanceTime(100); + + v = std::static_pointer_cast(root->findComponentById("VIDEO")); + ASSERT_EQ(v->isDisallowed(), true); + // No media player when disallow is true + ASSERT_EQ(v->getMediaPlayer(), nullptr); +} + +TEST_F(VideoComponentTest, ComponentNotDisplayedWhenDisallowVideoTrue) { + config->set(RootProperty::kDisallowVideo, true); + + loadDocument(VIDEO_IN_CONTAINER); + + ASSERT_TRUE(component); + // Inflated as expected + ASSERT_EQ(1, component->getChildCount()); + ASSERT_EQ(kComponentTypeVideo, component->getCoreChildAt(0)->getType()); + // Not displayed + ASSERT_EQ(0, component->getDisplayedChildCount()); +} + +TEST_F(VideoComponentTest, ComponentDisplayedWhenDisallowVideoFalse) { + config->set(RootProperty::kDisallowVideo, false); + + loadDocument(VIDEO_IN_CONTAINER); + + ASSERT_TRUE(component); + // Inflated as expected + ASSERT_EQ(1, component->getChildCount()); + ASSERT_EQ(kComponentTypeVideo, component->getCoreChildAt(0)->getType()); + // Displayed + ASSERT_EQ(1, component->getDisplayedChildCount()); + ASSERT_EQ(kComponentTypeVideo, component->getDisplayedChildAt(0)->getType()); +} \ No newline at end of file diff --git a/aplcore/unit/content/CMakeLists.txt b/aplcore/unit/content/CMakeLists.txt index 8592cd5..3a0cab6 100644 --- a/aplcore/unit/content/CMakeLists.txt +++ b/aplcore/unit/content/CMakeLists.txt @@ -19,5 +19,6 @@ target_sources_local(unittest unittest_document_background.cpp unittest_jsondata.cpp unittest_metrics.cpp + unittest_packages.cpp unittest_rootconfig.cpp ) \ No newline at end of file diff --git a/aplcore/unit/content/unittest_document.cpp b/aplcore/unit/content/unittest_document.cpp index 3b5dfad..9e633f3 100644 --- a/aplcore/unit/content/unittest_document.cpp +++ b/aplcore/unit/content/unittest_document.cpp @@ -640,12 +640,12 @@ TEST(DocumentTest, MultipleDependencies) // The requested list is cleared ASSERT_EQ(0, doc->getRequestedPackages().size()); - for (auto it = requested.begin() ; it != requested.end() ; it++) { - auto s = it->reference().name(); + for (const auto& request : requested) { + auto s = request.reference().name(); if (s == "A") - doc->addPackage(*it, DIAMOND_A); + doc->addPackage(request, DIAMOND_A); else if (s == "B") - doc->addPackage(*it, DIAMOND_B); + doc->addPackage(request, DIAMOND_B); else FAIL() << "Unrecognized package " << s; } @@ -740,12 +740,12 @@ TEST(DocumentTest, Duplicate) // The requested list is cleared ASSERT_EQ(0, doc->getRequestedPackages().size()); - for (auto it = requested.begin() ; it != requested.end() ; it++) { - auto s = it->reference().version(); + for (const auto& it : requested) { + auto s = it.reference().version(); if (s == "1.2") - doc->addPackage(*it, DUPLICATE_A_1_2); + doc->addPackage(it, DUPLICATE_A_1_2); else if (s == "2.2") - doc->addPackage(*it, DUPLICATE_A_2_2); + doc->addPackage(it, DUPLICATE_A_2_2); else FAIL() << "Unrecognized package " << s; } @@ -774,7 +774,7 @@ const char *FAKE_MAIN_TEMPLATE = R"apl({ })apl"; static std::string -makeTestPackage(std::vector dependencies, std::map stringMap) +makeTestPackage(const std::vector& dependencies, std::map stringMap) { rapidjson::Document doc; doc.SetObject(); @@ -813,7 +813,7 @@ makeTestPackage(std::vector dependencies, std::map writer(buffer); doc.Accept(writer); - return std::string(buffer.GetString()); + return {buffer.GetString()}; } TEST(DocumentTest, Generated) @@ -883,7 +883,7 @@ TEST(DocumentTest, Loop) ASSERT_TRUE(content); ASSERT_FALSE(content->isReady()); ASSERT_TRUE(content->isWaiting()); - for (auto it : content->getRequestedPackages()) { + for (const auto& it : content->getRequestedPackages()) { if (it.reference().name() == "A") content->addPackage(it, pkg_a); else if (it.reference().name() == "B") content->addPackage(it, pkg_b); else FAIL() << "Unknown package " << it.reference().name(); @@ -905,7 +905,7 @@ TEST(DocumentTest, NonReversal) ASSERT_TRUE(content); ASSERT_TRUE(content->isWaiting()); - for (auto it : content->getRequestedPackages()) { + for (const auto& it : content->getRequestedPackages()) { if (it.reference().name() == "A") content->addPackage(it, pkg_a); else if (it.reference().name() == "B") content->addPackage(it, pkg_b); else FAIL() << "Unknown package " << it.reference().name(); @@ -935,7 +935,7 @@ TEST(DocumentTest, Reversal) ASSERT_TRUE(content); ASSERT_TRUE(content->isWaiting()); - for (auto it : content->getRequestedPackages()) { + for (const auto& it : content->getRequestedPackages()) { if (it.reference().name() == "A") content->addPackage(it, pkg_a); else if (it.reference().name() == "B") content->addPackage(it, pkg_b); else FAIL() << "Unknown package " << it.reference().name(); @@ -968,7 +968,7 @@ TEST(DocumentTest, DeepReversal) ASSERT_TRUE(content); while (content->isWaiting()) { - for (auto it : content->getRequestedPackages()) { + for (const auto& it : content->getRequestedPackages()) { auto pkg = packageMap.find(it.reference().name()); if (pkg == packageMap.end()) FAIL() << "Unknown package " << it.reference().name(); @@ -1002,7 +1002,7 @@ TEST(DocumentTest, DeepLoop) ASSERT_TRUE(content); while (content->isWaiting()) { - for (auto it : content->getRequestedPackages()) { + for (const auto& it : content->getRequestedPackages()) { auto pkg = packageMap.find(it.reference().name()); if (pkg == packageMap.end()) FAIL() << "Unknown package " << it.reference().name(); @@ -1223,7 +1223,7 @@ TEST(DocumentTest, LogId) ASSERT_TRUE(doc); - ASSERT_EQ(session->getLogId() + ":content.cpp:Content : Initializing experience using " + + ASSERT_EQ(session->getLogId() + ":content.cpp:init : Initializing experience using " + std::string(sCoreRepositoryVersion), logBridge->mLog); logBridge->reset(); @@ -1264,7 +1264,7 @@ TEST(DocumentTest, ShortLogId) ASSERT_TRUE(doc); ASSERT_TRUE(session->getLogId().rfind("FOOBAR-", 0) == 0); - ASSERT_EQ(session->getLogId() + ":content.cpp:Content : Initializing experience using " + std::string(sCoreRepositoryVersion), logBridge->mLog); + ASSERT_EQ(session->getLogId() + ":content.cpp:init : Initializing experience using " + std::string(sCoreRepositoryVersion), logBridge->mLog); logBridge->reset(); LOG(LogLevel::kInfo).session(session) << "TEST"; @@ -1283,14 +1283,14 @@ TEST(DocumentTest, TwoDocuments) auto content1 = Content::create(LOG_ID_WITH_PREFIX, session1); ASSERT_TRUE(content1->isReady()); ASSERT_TRUE(content1->getSession()->getLogId().rfind("FOOBAR-", 0) == 0); - ASSERT_EQ(content1->getSession()->getLogId() + ":content.cpp:Content : Initializing experience using " + + ASSERT_EQ(content1->getSession()->getLogId() + ":content.cpp:init : Initializing experience using " + std::string(sCoreRepositoryVersion), logBridge->mLog); auto session2 = makeDefaultSession(); auto content2 = Content::create(LOG_ID_WITH_PREFIX, session2); ASSERT_TRUE(content2->isReady()); ASSERT_TRUE(content2->getSession()->getLogId().rfind("FOOBAR-", 0) == 0); - ASSERT_EQ(content2->getSession()->getLogId() + ":content.cpp:Content : Initializing experience using " + + ASSERT_EQ(content2->getSession()->getLogId() + ":content.cpp:init : Initializing experience using " + std::string(sCoreRepositoryVersion), logBridge->mLog); @@ -1378,19 +1378,19 @@ TEST(DocumentTest, NestedRepeatedImportPendingDoesNotRequest) // The requested list is cleared ASSERT_EQ(0, doc->getRequestedPackages().size()); - for (auto it = requested.begin() ; it != requested.end() ; it++) { - auto s = it->reference(); + for (const auto& it : requested) { + auto s = it.reference(); if (s == ImportRef("A", "1.0")) - doc->addPackage(*it, A_IMPORTS_B); + doc->addPackage(it, A_IMPORTS_B); } // We don't request "B" again even though "A" imports it with a different version ASSERT_EQ(0, doc->getRequestedPackages().size()); - for (auto it = requested.begin() ; it != requested.end() ; it++) { - auto s = it->reference(); + for (const auto& it : requested) { + auto s = it.reference(); if (s == ImportRef("B", "1.0")) - doc->addPackage(*it, B); + doc->addPackage(it, B); } ASSERT_FALSE(doc->isWaiting()); @@ -1411,16 +1411,16 @@ TEST(DocumentTest, NestedRepeatedImportLoadedDoesNotRequest) // The requested list is cleared ASSERT_EQ(0, doc->getRequestedPackages().size()); - for (auto it = requested.begin() ; it != requested.end() ; it++) { - auto s = it->reference(); + for (const auto& it : requested) { + auto s = it.reference(); if (s == ImportRef("B", "1.0")) - doc->addPackage(*it, B); + doc->addPackage(it, B); } - for (auto it = requested.begin() ; it != requested.end() ; it++) { - auto s = it->reference(); + for (const auto& it : requested) { + auto s = it.reference(); if (s == ImportRef("A", "1.0")) - doc->addPackage(*it, A_IMPORTS_B); + doc->addPackage(it, A_IMPORTS_B); } ASSERT_EQ(0, doc->getRequestedPackages().size()); @@ -1465,10 +1465,10 @@ TEST(DocumentTest, RepeatedImportDifferentSources) // The requested list is cleared ASSERT_EQ(0, doc->getRequestedPackages().size()); - for (auto it = requested.begin() ; it != requested.end() ; it++) { - auto s = it->reference(); - if (s == ImportRef("B", "1.0") && it->source() == "custom.json") - doc->addPackage(*it, B); + for (const auto& it : requested) { + auto s = it.reference(); + if (s == ImportRef("B", "1.0") && it.source() == "custom.json") + doc->addPackage(it, B); } ASSERT_EQ(0, doc->getRequestedPackages().size()); diff --git a/aplcore/unit/content/unittest_document_background.cpp b/aplcore/unit/content/unittest_document_background.cpp index 42e2334..8b6c2c4 100644 --- a/aplcore/unit/content/unittest_document_background.cpp +++ b/aplcore/unit/content/unittest_document_background.cpp @@ -75,6 +75,10 @@ TEST_F(DocumentBackgroundTest, ColorBackground) ASSERT_TRUE(background.is()); ASSERT_TRUE(IsEqual(Color(Color::BLUE), background)); + + // New API will fail + auto content = Content::create(COLOR_BACKGROUND, makeDefaultSession()); + ASSERT_TRUE(IsEqual(Color(Color::TRANSPARENT), content->getBackground())); } static const char *GRADIENT_BACKGROUND = R"({ @@ -232,3 +236,12 @@ TEST_F(DocumentBackgroundTest, DataBoundThemeOverride) ASSERT_TRUE(background.is()); ASSERT_TRUE(IsEqual(Color(0xe0e0c0ff), background)); } + +TEST_F(DocumentBackgroundTest, NewContentApi) +{ + metrics.theme("dark"); + auto content = Content::create(DATA_BOUND_THEME_OVERRIDE, makeDefaultSession(), metrics, config); + auto background = content->getBackground(); + ASSERT_TRUE(background.is()); + ASSERT_TRUE(IsEqual(Color(0xe0e0c0ff), background)); +} diff --git a/aplcore/unit/content/unittest_metrics.cpp b/aplcore/unit/content/unittest_metrics.cpp index 1ea1209..4ee2807 100644 --- a/aplcore/unit/content/unittest_metrics.cpp +++ b/aplcore/unit/content/unittest_metrics.cpp @@ -24,8 +24,7 @@ TEST_F(MetricsTest, Basic) { auto m = Metrics() .theme("floppy") .size(300, 400) - .autoSizeWidth(true) - .autoSizeHeight(false) + .minAndMaxWidth(200, 500) .dpi(320) .shape(ScreenShape::ROUND) .mode(ViewportMode::kViewportModePC); @@ -35,6 +34,10 @@ TEST_F(MetricsTest, Basic) { ASSERT_EQ(150, m.getWidth()); // Scaling factor of 160/320 ASSERT_TRUE(m.getAutoWidth()); ASSERT_FALSE(m.getAutoHeight()); + ASSERT_EQ(100, m.getMinWidth()); // Scaling factor of 160/320 + ASSERT_EQ(250, m.getMaxWidth()); // Scaling factor of 160/320 + ASSERT_EQ(200, m.getMinHeight()); // Scaling factor of 160/320 + ASSERT_EQ(200, m.getMaxHeight()); // Scaling factor of 160/320 ASSERT_EQ(320, m.getDpi()); ASSERT_EQ(ScreenShape::ROUND, m.getScreenShape()); ASSERT_EQ(ViewportMode::kViewportModePC, m.getViewportMode()); diff --git a/aplcore/unit/content/unittest_packages.cpp b/aplcore/unit/content/unittest_packages.cpp new file mode 100644 index 0000000..472429b --- /dev/null +++ b/aplcore/unit/content/unittest_packages.cpp @@ -0,0 +1,2411 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "gtest/gtest.h" + +#include "../testeventloop.h" + +#include "../embed/testdocumentmanager.h" + +using namespace apl; + +class PackagesTest : public DocumentWrapper { +public: + PackagesTest() {} + + bool process(const ContentPtr& c) { + if (!c->isWaiting()) return false; + + auto imports = c->getRequestedPackages(); + while (!imports.empty()) { + for (auto& req : imports) { + auto pack = mPackageStore.find(req.reference().toString() + ":" + req.source()); + if (pack != mPackageStore.end()) { + c->addPackage(req, pack->second); + } else { + c->addPackage(req, ""); + } + } + + imports = c->getRequestedPackages(); + } + + return true; + } + + void add(const std::string& name, const std::string& source, const std::string& package) { + mPackageStore.emplace(name + ":" + source, package); + } + + void add(const std::string& name, const std::string& package) { + add(name, "", package); + } + + void reset() { + mPackageStore.clear(); + } + +private: + std::map mPackageStore; +}; + +static const char *MAIN = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "basic", + "version": "1.2" + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +static const char *BASIC = R"apl({ + "type": "APL", + "version": "2023.3", + "resources": [ + { + "colors": { + "MyRed": "#ff0101ff" + } + } + ] +})apl"; + +TEST_F(PackagesTest, BasicOld) +{ + content = Content::create(MAIN, session); + ASSERT_TRUE(content); + + // The document has one import it is waiting for + ASSERT_TRUE(content->isWaiting()); + auto requested = content->getRequestedPackages(); + ASSERT_EQ(1, requested.size()); + + auto request = *requested.begin(); + ASSERT_STREQ("basic", request.reference().name().c_str()); + ASSERT_STREQ("1.2", request.reference().version().c_str()); + content->addPackage(request, BASIC); + ASSERT_FALSE(content->isWaiting()); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, BasicNew) +{ + add("basic:1.2", BASIC); + content = Content::create(MAIN, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *THEME_BASED_INCLUDE = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "basic", + "version": "1.2", + "when": "${environment.hasMagic != 'magic'}" + }, + { + "name": "conditional", + "version": "1.2", + "when": "${environment.hasMagic == 'magic'}" + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +static const char *CONDITIONAL = R"apl({ + "type": "APL", + "version": "2023.3", + "resources": [ + { + "colors": { + "MyRed": "#ff0000ff" + } + } + ] +})apl"; + +TEST_F(PackagesTest, ThemeConditionalNotSpecified) +{ + add("basic:1.2", BASIC); + content = Content::create(THEME_BASED_INCLUDE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ThemeConditionalSpecified) +{ + config->setEnvironmentValue("hasMagic", "magic"); + add("conditional:1.2", CONDITIONAL); + content = Content::create(THEME_BASED_INCLUDE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *STYLED_FRAME = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "basic", + "version": "1.2", + "when": "${environment.hasMagic != 'magic'}" + }, + { + "name": "conditional", + "version": "1.2", + "when": "${environment.hasMagic == 'magic'}" + } + ], + "layouts": { + "StyledFrame": { + "item": { + "type": "Frame", + "id": "magicFrame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } + } +})apl"; + +static const char *THEME_BASED_NESTED_INCLUDE = R"apl({ + "type": "APL", + "version": "2023.3", + "onConfigChange": { + "type": "Reinflate" + }, + "import": [ + { + "name": "StyledFrame", + "version": "1.0" + } + ], + "mainTemplate": { + "item": { + "type": "StyledFrame" + } + } +})apl"; + +TEST_F(PackagesTest, ThemeNestedConditionalNotSpecified) +{ + add("StyledFrame:1.0", STYLED_FRAME); + add("basic:1.2", BASIC); + content = Content::create(THEME_BASED_NESTED_INCLUDE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ThemeNestedConditionalSpecified) +{ + config->setEnvironmentValue("hasMagic", "magic"); + add("StyledFrame:1.0", STYLED_FRAME); + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + content = Content::create(THEME_BASED_NESTED_INCLUDE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *STYLED_FRAME_OVERRIDE = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "basic", + "version": "1.2" + }, + { + "name": "conditional", + "version": "1.2", + "when": "${environment.hasMagic == 'magic'}" + } + ], + "layouts": { + "StyledFrame": { + "item": { + "type": "Frame", + "id": "magicFrame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } + } +})apl"; + +TEST_F(PackagesTest, ThemeNestedConditionalOverride) +{ + config->setEnvironmentValue("hasMagic", "magic"); + add("StyledFrame:1.0", STYLED_FRAME_OVERRIDE); + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + + content = Content::create(THEME_BASED_NESTED_INCLUDE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *STYLED_FRAME_OVERRIDE_DEPENDS = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "conditional", + "version": "1.2", + "when": "${environment.hasMagic == 'magic'}", + "loadAfter": "dbasic" + }, + { + "name": "dbasic", + "description": "force it to to be requested later", + "version": "1.2" + } + ], + "layouts": { + "StyledFrame": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } + } +})apl"; + +TEST_F(PackagesTest, ThemeNestedConditionalOverrideDepends) +{ + config->setEnvironmentValue("hasMagic", "magic"); + add("StyledFrame:1.0", STYLED_FRAME_OVERRIDE_DEPENDS); + add("dbasic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + + content = Content::create(THEME_BASED_NESTED_INCLUDE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *EVALUATION_EVERYWHERE = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "when": "${environment.customPackageName}", + "name": "${environment.customPackageName}", + "version": "${environment.customPackageVersion}", + "source": "${environment.customPackageLocation}", + "loadAfter": "${environment.loadAfter}" + }, + { + "name": "dependency-package", + "version": "1.0" + } + ], + "mainTemplate": { + "item": { + "type": "Frame" + } + } +})apl"; + +TEST_F(PackagesTest, EvaluationEverywhere) +{ + // Just needs to sort before default + config->setEnvironmentValue("customPackageName", "bigNastyPackage"); + config->setEnvironmentValue("customPackageVersion", "1.2"); + config->setEnvironmentValue("customPackageLocation", "custom-location"); + config->setEnvironmentValue("loadAfter", "dependency-package"); + + add("bigNastyPackage:1.2", "custom-location", CONDITIONAL); + add("dependency-package:1.0", BASIC); + + content = Content::create(EVALUATION_EVERYWHERE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); +} + +static const char *METRICS_AND_VERSION_AVAILABLE = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "${'styles-' + viewport.mode + '-' + viewport.theme}", + "version": "${environment.documentAPLVersion}" + } + ], + "mainTemplate": { + "item": { + "type": "Frame" + } + } +})apl"; + +TEST_F(PackagesTest, MetricsAndVersionAvailable) +{ + metrics.mode(apl::kViewportModeMobile).theme("light"); + add("styles-mobile-light:2023.3", CONDITIONAL); + content = Content::create(METRICS_AND_VERSION_AVAILABLE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); +} + +static const char *CIRCULAR_DEPENDS = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "A", + "version": "A", + "loadAfter": "B" + }, + { + "name": "B", + "version": "B", + "loadAfter": "A" + } + ], + "mainTemplate": { + "item": { + "type": "Frame" + } + } +})apl"; + +TEST_F(PackagesTest, CircularDepends) +{ + add("A:A", CONDITIONAL); + add("B:B", BASIC); + content = Content::create(CIRCULAR_DEPENDS, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_FALSE(content->isReady()); + ASSERT_TRUE(content->isError()); + + // Complains about circular dep. + ASSERT_TRUE(session->checkAndClear("Failure to order packages")); +} + +static const char *DEPENDS_ON_ITSELF = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "A", + "version": "A", + "loadAfter": "A" + } + ], + "mainTemplate": { + "item": { + "type": "Frame" + } + } +})apl"; + +TEST_F(PackagesTest, DependsOnItself) +{ + add("A:A", CONDITIONAL); + content = Content::create(DEPENDS_ON_ITSELF, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_FALSE(content->isWaiting()); + ASSERT_TRUE(session->checkAndClear("Malformed package import record")); +} + +static const char *MULTI_DEPENDS = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "A", + "version": "A", + "loadAfter": "B" + }, + { + "name": "B", + "version": "B", + "loadAfter": [ "C", "D" ] + }, + { + "name": "C", + "version": "C", + "loadAfter": "D" + }, + { + "name": "D", + "version": "D" + } + ], + "mainTemplate": { + "item": { + "type": "Frame" + } + } +})apl"; + +TEST_F(PackagesTest, MultiDepends) +{ + add("A:A", BASIC); + add("B:B", BASIC); + add("C:C", BASIC); + add("D:D", BASIC); + content = Content::create(MULTI_DEPENDS, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); +} + +static const char *MULTI_DEPENDS_CYCLE = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "A", + "version": "A", + "loadAfter": "B" + }, + { + "name": "B", + "version": "B", + "loadAfter": [ "C", "D" ] + }, + { + "name": "C", + "version": "C", + "loadAfter": "D" + }, + { + "name": "D", + "version": "D", + "loadAfter": "B" + } + ], + "mainTemplate": { + "item": { + "type": "Frame" + } + } +})apl"; + +TEST_F(PackagesTest, MultiDependsCycle) +{ + add("A:A", BASIC); + add("B:B", BASIC); + add("C:C", BASIC); + add("D:D", BASIC); + content = Content::create(MULTI_DEPENDS_CYCLE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_FALSE(content->isReady()); + ASSERT_TRUE(session->checkAndClear("Circular package loadAfter dependency between D and B")); +} + +static const char* HOST_DOC = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Host", + "width": "100%", + "height": "100%", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + } + } +})"; + +TEST_F(PackagesTest, EmbeddedDoc) +{ + std::shared_ptr documentManager = std::make_shared(); + config->documentManager(std::static_pointer_cast(documentManager)); + + add("StyledFrame:1.0", STYLED_FRAME_OVERRIDE); + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + + content = Content::create(HOST_DOC, session, metrics, *config); + ASSERT_TRUE(content->isReady()); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().empty()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + auto content = Content::create(THEME_BASED_NESTED_INCLUDE, session); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().size()); + + auto request = documentManager->get("embeddedDocumentUrl").lock(); + content->refresh(*request, nullptr); + + // Content becomes "Waiting again" + ASSERT_TRUE(content->isWaiting()); + // Re-resolve + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, + DocumentConfig::create(), true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_EQ(0xff0101ff, root->findComponentById("magicFrame")->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ChangeConfigAfterContentInitialization) +{ + add("StyledFrame:1.0", STYLED_FRAME_OVERRIDE_DEPENDS); + add("dbasic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + + content = Content::create(THEME_BASED_NESTED_INCLUDE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + // Config (or metrics/or both) changed when RootContext creation possible. Should still account + // for it. + config->setEnvironmentValue("hasMagic", "magic"); + content->refresh(metrics, *config); + + // Content becomes "Waiting again" + ASSERT_TRUE(content->isWaiting()); + // Re-resolve + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *BLUE = R"apl({ + "type": "APL", + "version": "2023.3", + "resources": [ + { + "colors": { + "MyBlue": "#0101ffff" + } + } + ] +})apl"; + +static const char *MAIN_RED_BLUE = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "red", + "version": "1.0" + }, + { + "name": "blue", + "version": "1.0" + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "borderColor": "@MyBlue", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, RefreshUsesStashedPackages) +{ + add("red:1.0", BASIC); + add("blue:1.0", BLUE); + + content = Content::create(MAIN_RED_BLUE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + // Refresh it + content->refresh(metrics, *config); + + // Use of stashed packages means no re-processing needed + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); + ASSERT_EQ(0x0101ffff, component->getCalculated(apl::kPropertyBorderColor).getColor()); +} + +TEST_F(PackagesTest, ChangeConfigAfterContentInitializationReused) +{ + add("StyledFrame:1.0", STYLED_FRAME_OVERRIDE_DEPENDS); + add("dbasic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + + content = Content::create(THEME_BASED_NESTED_INCLUDE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + // Replace package manager with empty one + reset(); + // Should reuse already loaded stuff and succeed + content->refresh(metrics, *config); + + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ConditionalReinflate) +{ + add("StyledFrame:1.0", STYLED_FRAME_OVERRIDE_DEPENDS); + add("dbasic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + + content = Content::create(THEME_BASED_NESTED_INCLUDE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); + + auto configChange = ConfigurationChange().environmentValue("hasMagic", "magic"); + root->configurationChange(configChange); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeReinflate, event.getType()); + + // So we check related content and re-resolve it. + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + // Now reinflate + root->reinflate(); + context = root->contextPtr(); + ASSERT_TRUE(context); + ASSERT_TRUE(context->getReinflationFlag()); + component = CoreComponent::cast(root->topComponent()); + + // And resolve + if (event.getActionRef().isPending()) { + event.getActionRef().resolve(); + } + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ConditionalEmbedded) +{ + std::shared_ptr documentManager = std::make_shared(); + config->documentManager(std::static_pointer_cast(documentManager)); + + add("StyledFrame:1.0", STYLED_FRAME_OVERRIDE); + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + + content = Content::create(HOST_DOC, session, metrics, *config); + ASSERT_TRUE(content->isReady()); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().empty()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + auto content = Content::create(THEME_BASED_NESTED_INCLUDE, session); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().size()); + + auto request = documentManager->get("embeddedDocumentUrl").lock(); + auto documentConfig = DocumentConfig::create(); + documentConfig->setEnvironmentValue("hasMagic", "magic"); + + content->refresh(*request, documentConfig); + + // Content becomes "Waiting again" + ASSERT_TRUE(content->isWaiting()); + // Re-resolve + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, + documentConfig, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_EQ(0xff0000ff, root->findComponentById("magicFrame")->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *SELECTOR = R"apl({ + "type": "APL", + "version": "2023.3", + "onConfigChange": { + "type": "Reinflate" + }, + "import": [ + { + "type": "oneOf", + "items": [ + { + "name": "another-conditional", + "version": "1.2", + "when": "${environment.moreMagic == 'magic'}" + }, + { + "name": "conditional", + "version": "1.2", + "when": "${environment.hasMagic == 'magic'}" + }, + { + "name": "basic", + "version": "1.2" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +static const char *MORE_CONDITIONAL = R"apl({ + "type": "APL", + "version": "2023.3", + "resources": [ + { + "colors": { + "MyRed": "#ff0202ff" + } + } + ] +})apl"; + +TEST_F(PackagesTest, ConditionalNotSpecifiedSelectOne) +{ + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + content = Content::create(SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ConditionalSpecifiedSelectOne) +{ + config->setEnvironmentValue("hasMagic", "magic"); + + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + content = Content::create(SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ConditionalSpecifiedMultipleSelectOne) +{ + config->setEnvironmentValue("hasMagic", "magic"); + config->setEnvironmentValue("moreMagic", "magic"); + + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + content = Content::create(SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0202ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ConditionalSelectOneReinflate) +{ + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + content = Content::create(SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); + + auto configChange = ConfigurationChange().environmentValue("hasMagic", "magic"); + root->configurationChange(configChange); + root->clearPending(); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeReinflate, event.getType()); + + // So we check related content and re-resolve it. + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + // Now reinflate + root->reinflate(); + context = root->contextPtr(); + ASSERT_TRUE(context); + ASSERT_TRUE(context->getReinflationFlag()); + component = CoreComponent::cast(root->topComponent()); + + // And resolve + if (event.getActionRef().isPending()) { + event.getActionRef().resolve(); + } + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ConditionalSelectOneReinflateAfterFailure) +{ + add("basic:1.2", BASIC); + add("another-conditional:1.2", MORE_CONDITIONAL); + + content = Content::create(SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); + + // ConfigChange to a missing package + auto configChange = ConfigurationChange().environmentValue("hasMagic", "magic"); + root->configurationChange(configChange); + root->clearPending(); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeReinflate, event.getType()); + + // There is no conditional package so expects the failure + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isError()); + ASSERT_TRUE(session->checkAndClear()); + + // ConfigChange to an existing package + configChange = ConfigurationChange().environmentValue("moreMagic", "magic"); + root->configurationChange(configChange); + root->clearPending(); + + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(kEventTypeReinflate, event.getType()); + + // So we check related content and re-resolve it. + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + // Now reinflate + root->reinflate(); + context = root->contextPtr(); + ASSERT_TRUE(context); + ASSERT_TRUE(context->getReinflationFlag()); + component = CoreComponent::cast(root->topComponent()); + + // And resolve + if (event.getActionRef().isPending()) { + event.getActionRef().resolve(); + } + + ASSERT_EQ(0xff0202ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *COMPLEX_SELECTOR = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "oneOf", + "items": [ + { + "name": "first-block-conditional", + "version": "1.2", + "when": "${environment.moreMagic == 'magic'}", + "loadAfter": "second-block-conditional" + }, + { + "name": "first-block-fallback", + "version": "1.2", + "loadAfter": "second-block-fallback" + } + ] + }, + { + "name": "non-selector-conditional", + "when": "${environment.moreMagic == 'magic'}", + "version": "1.2", + "loadAfter": "first-block-conditional" + }, + { + "name": "non-selector-more-conditional", + "when": "${environment.moreMagic != 'magic'}", + "version": "1.2", + "loadAfter": "first-block-fallback" + }, + { + "type": "oneOf", + "items": [ + { + "name": "second-block-conditional", + "version": "1.2", + "when": "${environment.moreMagic == 'magic'}" + }, + { + "name": "second-block-fallback", + "version": "1.2" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, ComplexSelectorNoConditional) +{ + add("first-block-fallback:1.2", BASIC); + add("first-block-conditional:1.2", BASIC); + add("second-block-fallback:1.2", BASIC); + add("second-block-conditional:1.2", BASIC); + + add("non-selector-conditional:1.2", CONDITIONAL); + add("non-selector-more-conditional:1.2", MORE_CONDITIONAL); + + content = Content::create(COMPLEX_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0202ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ComplexSelectorConditional) +{ + add("first-block-fallback:1.2", BASIC); + add("first-block-conditional:1.2", BASIC); + add("second-block-fallback:1.2", BASIC); + add("second-block-conditional:1.2", BASIC); + + add("non-selector-conditional:1.2", CONDITIONAL); + add("non-selector-more-conditional:1.2", MORE_CONDITIONAL); + + config->setEnvironmentValue("moreMagic", "magic"); + + content = Content::create(COMPLEX_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char* STALE_HOST_DOC = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Host", + "width": "100%", + "height": "100%", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ], + "environment": { "hasMagic": "${environment.hasMagic}" } + } + } +})"; + +static const char *THEME_BASED_CONDITIONAL = R"apl({ + "type": "APL", + "version": "2023.3", + "onConfigChange": { + "type": "Reinflate" + }, + "import": [ + { + "type": "oneOf", + "items": [ + { + "name": "conditional", + "version": "1.2", + "when": "${viewport.theme == 'magic'}" + }, + { + "name": "basic", + "version": "1.2" + } + ] + } + ], + "layouts": { + "StyledFrame": { + "item": { + "type": "Frame", + "id": "magicFrame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } + }, + "mainTemplate": { + "item": { + "type": "StyledFrame", + "id": "magicFrame" + } + } +})apl"; + +TEST_F(PackagesTest, ConditionalEmbeddedReinflateTheme) +{ + std::shared_ptr documentManager = std::make_shared(); + config->documentManager(std::static_pointer_cast(documentManager)); + + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + + content = Content::create(STALE_HOST_DOC, session, metrics, *config); + ASSERT_TRUE(content->isReady()); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().empty()); + + root = std::static_pointer_cast(RootContext::create(metrics, content, *config)); + ASSERT_TRUE(root); + + auto embeddedContent = Content::create(THEME_BASED_CONDITIONAL, session); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().size()); + + auto request = documentManager->get("embeddedDocumentUrl").lock(); + auto documentConfig = DocumentConfig::create(); + embeddedContent->refresh(*request, documentConfig); + + // Content becomes "Waiting again" + ASSERT_TRUE(embeddedContent->isWaiting()); + // Re-resolve + ASSERT_TRUE(process(embeddedContent)); + ASSERT_TRUE(embeddedContent->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", embeddedContent, true, + documentConfig, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_EQ(0xff0101ff, root->findComponentById("magicFrame")->getCalculated(apl::kPropertyBackgroundColor).getColor()); + + // Reinflate + + auto configChange = ConfigurationChange().theme("magic"); + root->configurationChange(configChange); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeContentRefresh, event.getType()); + auto doc = event.getDocument(); + ASSERT_EQ(embeddedDocumentContext, doc); + + ASSERT_TRUE(embeddedContent->isWaiting()); + ASSERT_TRUE(process(embeddedContent)); + ASSERT_TRUE(embeddedContent->isReady()); + + event.getActionRef().resolve(); + + advanceTime(100); + ASSERT_EQ(0xff0000ff, root->findComponentById("magicFrame")->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *DEEP_SELECTOR = R"apl({ + "type": "APL", + "version": "2023.3", + "onConfigChange": { + "type": "Reinflate" + }, + "import": [ + { + "type": "oneOf", + "items": [ + { + "type": "oneOf", + "when": "${environment.hasMagic == 'magic'}", + "items": [ + { + "name": "another-conditional", + "version": "1.2", + "when": "${environment.moreMagic == 'magic'}" + }, + { + "type": "package", + "name": "conditional", + "version": "1.2" + } + ] + }, + { + "type": "package", + "name": "basic", + "version": "1.2" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, ConditionalDeepSelectorNoConditional) +{ + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + content = Content::create(DEEP_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ConditionalDeepSelectorConditional) +{ + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + config->setEnvironmentValue("hasMagic", "magic"); + + content = Content::create(DEEP_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ConditionalDeepSelectorMoreConditional) +{ + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + config->setEnvironmentValue("hasMagic", "magic"); + config->setEnvironmentValue("moreMagic", "magic"); + + content = Content::create(DEEP_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0202ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *SAME_NAME_SELECTOR = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "oneOf", + "name": "basic", + "version": "1.0", + "items": [ + { + "when": "${environment.moreMagic == 'magic'}", + "name": "another-conditional", + "source": "ac_url" + }, + { + "when": "${environment.hasMagic == 'magic'}", + "type": "package", + "version": "1.1", + "source": "c_url" + }, + { + "source": "basic_url" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, SelectorExpandedNameVersionNoConditional) +{ + add("basic:1.0", "basic_url", BASIC); + + content = Content::create(SAME_NAME_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, SelectorExpandedNameVersionConditional) +{ + add("basic:1.1", "c_url", CONDITIONAL); + + config->setEnvironmentValue("hasMagic", "magic"); + + content = Content::create(SAME_NAME_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, SelectorExpandedNameVersionMoreConditional) +{ + add("another-conditional:1.0", "ac_url", MORE_CONDITIONAL); + + config->setEnvironmentValue("moreMagic", "magic"); + + content = Content::create(SAME_NAME_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0202ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *OTHERWISE_SELECTOR = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "oneOf", + "name": "basic", + "version": "1.0", + "items": [ + { + "when": "${environment.moreMagic == 'magic'}", + "name": "another-conditional", + "source": "ac_url" + }, + { + "when": "${environment.hasMagic == 'magic'}", + "type": "package", + "version": "1.1", + "source": "c_url" + } + ], + "otherwise": [ + { + "source": "basic_url" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, SelectorExpandedNameVersionOtherwise) +{ + add("basic:1.0", "basic_url", BASIC); + + content = Content::create(OTHERWISE_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *OTHERWISE_MALFORMED = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "oneOf", + "items": [ + { + "when": "${environment.moreMagic == 'magic'}", + "name": "another-conditional", + "version": "1.0", + "source": "ac_url" + }, + { + "when": "${environment.hasMagic == 'magic'}", + "type": "package", + "name": "basic", + "version": "1.1", + "source": "c_url" + } + ], + "otherwise": [ + { + "source": "basic_url" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, SelectorOtherwiseFail) +{ + content = Content::create(OTHERWISE_MALFORMED, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isError()); + ASSERT_TRUE(session->checkAndClear("Otherwise imports failed")); +} + +static const char *OTHERWISE_EMPTY = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "oneOf", + "items": [ + { + "when": "${environment.moreMagic == 'magic'}", + "name": "another-conditional", + "version": "1.0", + "source": "ac_url" + }, + { + "when": "${environment.hasMagic == 'magic'}", + "type": "package", + "name": "basic", + "version": "1.1", + "source": "c_url" + } + ], + "otherwise": [] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, SelectorOtherwiseEmpty) +{ + content = Content::create(OTHERWISE_EMPTY, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isReady()); +} + +static const char *NO_ITEMS_SELECTOR = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "oneOf", + "name": "basic", + "version": "1.0", + "otherwise": [ + { + "source": "basic_url" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, SelectorNoItems) +{ + add("basic:1.0", "basic_url", BASIC); + + content = Content::create(NO_ITEMS_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isError()); + ASSERT_TRUE(session->checkAndClear("Missing items field for the oneOf import")); +} + +static const char *DEEP_NAME_SELECTOR = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "oneOf", + "name": "depending", + "version": "1.2", + "loadAfter": ["basic"], + "items": [ + { + "type": "oneOf", + "when": "${environment.hasMagic == 'magic'}", + "items": [ + { + "when": "${environment.hasMagic == 'magic'}", + "source": "DEEP_LOADED" + }, + { + "type": "package", + "source": "DEEP_UNLOADED" + } + ] + }, + { + "source": "SHALLOW_LOADED" + } + ] + }, + { + "name": "basic", + "version": "1.2" + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, ConditionalDeepNameSelectorNoConditional) +{ + add("basic:1.2", BASIC); + add("depending:1.2", "DEEP_LOADED", MORE_CONDITIONAL); + add("depending:1.2", "SHALLOW_LOADED", CONDITIONAL); + + content = Content::create(DEEP_NAME_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, ConditionalDeepNameSelectorConditional) +{ + add("basic:1.2", BASIC); + add("depending:1.2", "DEEP_LOADED", MORE_CONDITIONAL); + add("depending:1.2", "SHALLOW_LOADED", CONDITIONAL); + + config->setEnvironmentValue("hasMagic", "magic"); + + content = Content::create(DEEP_NAME_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0202ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *CONTENT_THEME_CONDITIONAL = R"apl({ + "type": "APL", + "version": "2023.3", + "onConfigChange": { + "type": "Reinflate" + }, + "import": [ + { + "type": "oneOf", + "items": [ + { + "name": "conditional", + "version": "1.2", + "when": "${viewport.theme == 'magic'}" + }, + { + "name": "basic", + "version": "1.2" + } + ] + } + ], + "layouts": { + "StyledFrame": { + "item": { + "type": "Frame", + "id": "magicFrame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } + }, + "mainTemplate": { + "item": { + "type": "StyledFrame", + "id": "magicFrame", + "onMount": { + "type": "SendEvent", + "delay": 1000, + "sequencer": "SEND_EVENT_MAYBE" + } + } + } +})apl"; + +TEST_F(PackagesTest, EmbeddedThemeConditionalPropagation) +{ + std::shared_ptr documentManager = std::make_shared(); + config->documentManager(std::static_pointer_cast(documentManager)); + + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + + content = Content::create(STALE_HOST_DOC, session, metrics, *config); + ASSERT_TRUE(content->isReady()); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().empty()); + + root = std::static_pointer_cast(RootContext::create(metrics, content, *config)); + ASSERT_TRUE(root); + + auto embeddedContent = Content::create(CONTENT_THEME_CONDITIONAL, session); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().size()); + + auto request = documentManager->get("embeddedDocumentUrl").lock(); + auto documentConfig = DocumentConfig::create(); + embeddedContent->refresh(*request, documentConfig); + + // Content becomes "Waiting again" + ASSERT_TRUE(embeddedContent->isWaiting()); + // Re-resolve + ASSERT_TRUE(process(embeddedContent)); + ASSERT_TRUE(embeddedContent->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", embeddedContent, true, + documentConfig, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_EQ(0xff0101ff, root->findComponentById("magicFrame")->getCalculated(apl::kPropertyBackgroundColor).getColor()); + + // Reinflate + + auto configChange = ConfigurationChange().theme("magic"); + root->configurationChange(configChange); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeContentRefresh, event.getType()); + auto doc = event.getDocument(); + ASSERT_EQ(embeddedDocumentContext, doc); + + advanceTime(1000); + + ASSERT_FALSE(root->hasEvent()); + + ASSERT_TRUE(embeddedContent->isWaiting()); + ASSERT_TRUE(process(embeddedContent)); + ASSERT_TRUE(embeddedContent->isReady()); + + event.getActionRef().resolve(); + + advanceTime(100); + ASSERT_EQ(0xff0000ff, root->findComponentById("magicFrame")->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *ALL_OF = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "oneOf", + "items": [ + { + "type": "allOf", + "when": "${environment.hasMagic == 'magic'}", + "items": [ + { + "name": "another-conditional", + "version": "1.2" + } + ] + }, + { + "type": "package", + "name": "basic", + "version": "1.2" + } + ] + }, + { + "type": "allOf", + "when": "${environment.moreMagic == 'magic'}", + "items": [ + { + "type": "package", + "name": "conditional", + "loadAfter": [ "basic" ], + "version": "1.2" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, AllOfNoConditional) +{ + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + content = Content::create(DEEP_SELECTOR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *ALL_OF_NO_ITEMS = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "allOf" + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, AllOfNoItems) +{ + content = Content::create(ALL_OF_NO_ITEMS, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isError()); + ASSERT_TRUE(session->checkAndClear("Missing items field for the allOf import")); +} + +TEST_F(PackagesTest, AllOfConditional) +{ + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + config->setEnvironmentValue("hasMagic", "magic"); + + content = Content::create(ALL_OF, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0202ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +TEST_F(PackagesTest, AllOfMoreConditional) +{ + add("basic:1.2", BASIC); + add("conditional:1.2", CONDITIONAL); + add("another-conditional:1.2", MORE_CONDITIONAL); + + config->setEnvironmentValue("moreMagic", "magic"); + + content = Content::create(ALL_OF, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0000ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + +static const char *NO_LOAD_AFTER = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "package", + "name": "salad", + "version": "1.2", + "loadAfter": [ "potatoes" ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, NoLoadAfter) +{ + add("salad:1.2", BASIC); + + content = Content::create(NO_LOAD_AFTER, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_FALSE(content->isReady()); + + ASSERT_TRUE(session->checkAndClear("Required loadAfter package not available potatoes for salad")); +} + +static const char *LONG_CIRCULAR = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "type": "package", + "name": "A", + "version": "1.2", + "loadAfter": [ "B" ] + }, + { + "type": "package", + "name": "B", + "version": "1.2", + "loadAfter": [ "C", "BB" ] + }, + { + "type": "package", + "name": "BB", + "version": "1.2" + }, + { + "type": "package", + "name": "C", + "version": "1.2", + "loadAfter": [ "A" ] + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } +})apl"; + +TEST_F(PackagesTest, LongCircularLoadDependency) +{ + add("A:1.2", BASIC); + add("B:1.2", BASIC); + add("BB:1.2", BASIC); + add("C:1.2", BASIC); + + content = Content::create(LONG_CIRCULAR, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_FALSE(content->isReady()); + + ASSERT_TRUE(session->checkAndClear("Failure to order packages")); +} + +#ifdef ALEXAEXTENSIONS +// Extensions support + +using namespace alexaext; + +class LittleTestExtension final : public alexaext::ExtensionBase { +public: + explicit LittleTestExtension(const std::string& uri) : ExtensionBase(std::set({uri})) {}; + + rapidjson::Document createRegistration(const std::string& uri, const rapidjson::Value& registerRequest) override { + std::string schema = R"({ + "type":"Schema", + "version":"1.0" + })"; + + rapidjson::Document doc; + doc.Parse(schema.c_str()); + doc.AddMember("uri", rapidjson::Value(uri.c_str(), doc.GetAllocator()), doc.GetAllocator()); + return RegistrationSuccess("1.0").uri(uri).token("SessionToken12").schema(doc); + } + + static std::shared_ptr createProxy(const std::string& uri) { + return std::make_shared(std::make_shared(uri)); + } +}; + +class LittleTestExtensionProvider : public alexaext::ExtensionRegistrar { +public : + std::function returnNullProxyPredicate = nullptr; + + ExtensionProxyPtr getExtension(const std::string& uri) { + return ExtensionRegistrar::getExtension(uri); + } +}; + +class LittleTestResourceProvider final: public ExtensionResourceProvider { +public: + bool requestResource(const std::string& uri, const std::string& resourceId, + ExtensionResourceSuccessCallback success, + ExtensionResourceFailureCallback error) override { + + // success callback if resource supported + auto resource = std::make_shared(resourceId); + success(uri, resource); + return true; + }; +}; + +static const char *SELECTOR_WITH_EXTENSIONS = R"apl({ + "type": "APL", + "version": "2023.3", + "onConfigChange": { + "type": "Reinflate" + }, + "import": [ + { + "type": "oneOf", + "items": [ + { + "name": "conditional", + "version": "1.2", + "when": "${environment.hasMagic == 'magic'}" + }, + { + "name": "basic", + "version": "1.2" + } + ] + } + ], + "mainTemplate": { + "item": { + "id": "magicText", + "type": "Text", + "width": "100%", + "height": "100%", + "text": "B: ${environment.extension.Basic} C: ${environment.extension.Conditional}" + } + } +})apl"; + +static const char *BASIC_WITH_EXTENSIONS = R"apl({ + "type": "APL", + "version": "2023.3", + "extensions": [ + { + "uri": "alexaext:basic:1.0", + "name": "Basic" + } + ] +})apl"; + +static const char *CONDITIONAL_WITH_EXTENSIONS = R"apl({ + "type": "APL", + "version": "2023.3", + "extensions": [ + { + "uri": "alexaext:conditional:1.0", + "name": "Conditional" + } + ] +})apl"; + +TEST_F(PackagesTest, ReinflateWithExtensions) +{ + auto extSession = ExtensionSession::create(); + auto extensionProvider = std::make_shared(); + auto resourceProvider = std::make_shared(); + auto mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + extensionProvider->registerExtension(LittleTestExtension::createProxy("alexaext:basic:1.0")); + extensionProvider->registerExtension(LittleTestExtension::createProxy("alexaext:conditional:1.0")); + + add("basic:1.2", BASIC_WITH_EXTENSIONS); + add("conditional:1.2", CONDITIONAL_WITH_EXTENSIONS); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + content = Content::create(SELECTOR_WITH_EXTENSIONS, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){}); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ("B: true C: ", component->getCalculated(apl::kPropertyText).asString()); + + auto configChange = ConfigurationChange().environmentValue("hasMagic", "magic"); + root->configurationChange(configChange); + root->clearPending(); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeReinflate, event.getType()); + + // So we check related content and re-resolve it. + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){}); + + // Now reinflate + root->reinflate(); + context = root->contextPtr(); + ASSERT_TRUE(context); + ASSERT_TRUE(context->getReinflationFlag()); + component = CoreComponent::cast(root->topComponent()); + + // And resolve + if (event.getActionRef().isPending()) { + event.getActionRef().resolve(); + } + + ASSERT_EQ("B: C: true", component->getCalculated(apl::kPropertyText).asString()); +} + +TEST_F(PackagesTest, ReinflateWithExtensionsEmbedded) +{ + auto extSession = ExtensionSession::create(); + auto extensionProvider = std::make_shared(); + auto resourceProvider = std::make_shared(); + auto mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + extensionProvider->registerExtension(LittleTestExtension::createProxy("alexaext:basic:1.0")); + extensionProvider->registerExtension(LittleTestExtension::createProxy("alexaext:conditional:1.0")); + + add("basic:1.2", BASIC_WITH_EXTENSIONS); + add("conditional:1.2", CONDITIONAL_WITH_EXTENSIONS); + + std::shared_ptr documentManager = std::make_shared(); + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator) + .documentManager(std::static_pointer_cast(documentManager)); + + content = Content::create(STALE_HOST_DOC, session, metrics, *config); + ASSERT_TRUE(content->isReady()); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().empty()); + + root = std::static_pointer_cast(RootContext::create(metrics, content, *config)); + ASSERT_TRUE(root); + + auto embeddedContent = Content::create(SELECTOR_WITH_EXTENSIONS, session); + + ASSERT_TRUE(documentManager->getUnresolvedRequests().size()); + + auto request = documentManager->get("embeddedDocumentUrl").lock(); + auto documentConfig = DocumentConfig::create(); + documentConfig->extensionMediator(mediator); + embeddedContent->refresh(*request, documentConfig); + + // Content becomes "Waiting again" + ASSERT_TRUE(embeddedContent->isWaiting()); + // Re-resolve + ASSERT_TRUE(process(embeddedContent)); + ASSERT_TRUE(embeddedContent->isReady()); + + mediator->initializeExtensions(ObjectMap{}, embeddedContent); + mediator->loadExtensions(ObjectMap{}, embeddedContent, [&](bool result){}); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", embeddedContent, true, + documentConfig, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_EQ("B: true C: ", root->findComponentById("magicText")->getCalculated(apl::kPropertyText).asString()); + + // Reinflate + + auto configChange = ConfigurationChange().environmentValue("hasMagic", "magic"); + root->configurationChange(configChange); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeContentRefresh, event.getType()); + auto doc = event.getDocument(); + ASSERT_EQ(embeddedDocumentContext, doc); + + ASSERT_TRUE(embeddedContent->isWaiting()); + ASSERT_TRUE(process(embeddedContent)); + ASSERT_TRUE(embeddedContent->isReady()); + + mediator->initializeExtensions(ObjectMap{}, embeddedContent); + mediator->loadExtensions(ObjectMap{}, embeddedContent, [&](bool result){}); + + event.getActionRef().resolve(); + + advanceTime(100); + ASSERT_EQ("B: C: true", root->findComponentById("magicText")->getCalculated(apl::kPropertyText).asString()); +} + +#endif // ALEXAEXTENSIONS diff --git a/aplcore/unit/content/unittest_rootconfig.cpp b/aplcore/unit/content/unittest_rootconfig.cpp index 475e079..907ef45 100644 --- a/aplcore/unit/content/unittest_rootconfig.cpp +++ b/aplcore/unit/content/unittest_rootconfig.cpp @@ -50,4 +50,15 @@ TEST(RootConfigTest, CannotShadowExistingNames) // Check that all invalid names have been rejected, so the environment still appears empty ASSERT_TRUE(rootConfig.getEnvironmentValues().empty()); +} + +TEST(RootConfigTest, RootPropertyBimapFullySynced) +{ + for (int rootProperty = RootProperty::kRootPropertySetBegin + 1; rootProperty != RootProperty::kRootPropertySetEnd; rootProperty++) { + auto it = sRootPropertyBimap.find(static_cast(rootProperty)); + if (it == sRootPropertyBimap.end()) { + LOG(apl::LogLevel::kError) << "Key " << static_cast(rootProperty) << " has not been assigned a name"; + } + ASSERT_TRUE(it != sRootPropertyBimap.end()); + } } \ No newline at end of file diff --git a/aplcore/unit/datagrammar/unittest_grammar.cpp b/aplcore/unit/datagrammar/unittest_grammar.cpp index 3f52f20..6c64259 100644 --- a/aplcore/unit/datagrammar/unittest_grammar.cpp +++ b/aplcore/unit/datagrammar/unittest_grammar.cpp @@ -492,6 +492,11 @@ TEST_F(GrammarTest, Functions) ASSERT_TRUE(IsEqual("ry", eval("${String.slice('berry', -2)}"))); ASSERT_TRUE(IsEqual("küss", eval("${String.slice('küssen', 0, -2)}"))); ASSERT_TRUE(IsEqual(u8"خوارزمی‎", eval(u8"${String.slice('محمد بن موسی خوارزمی‎', 13)}"))); + ASSERT_TRUE(IsEqual("r", eval("${String.charAt('berry', 2)}"))); + ASSERT_TRUE(IsEqual("y", eval("${String.charAt('berry', -1)}"))); + ASSERT_TRUE(IsEqual("ü", eval("${String.charAt('küssen', 1)}"))); + ASSERT_TRUE(IsEqual(u8"Ø®", eval(u8"${String.charAt('محمد بن موسی خوارزمی‎', 13)}"))); + // TODO: TEST_F LISTS OF 0, 1, and N arguments } @@ -1267,4 +1272,34 @@ TEST_F(GrammarTest, Delayed) << " expected=" << m.at(i + 1); } } -} \ No newline at end of file +} + +TEST_F(GrammarTest, LogHelpers) +{ + ASSERT_EQ(kCommandLogLevelDebug, eval("${Log.DEBUG}").asInt()); + ASSERT_EQ(kCommandLogLevelInfo, eval("${Log.INFO}").asInt()); + ASSERT_EQ(kCommandLogLevelWarn, eval("${Log.WARN}").asInt()); + ASSERT_EQ(kCommandLogLevelError, eval("${Log.ERROR}").asInt()); + ASSERT_EQ(kCommandLogLevelCritical, eval("${Log.CRITICAL}").asInt()); + + ASSERT_EQ(kCommandLogLevelDebug, eval("${Log.levelValue('debug')}").asInt()); + ASSERT_EQ(kCommandLogLevelInfo, eval("${Log.levelValue('info')}").asInt()); + ASSERT_EQ(kCommandLogLevelWarn, eval("${Log.levelValue('warn')}").asInt()); + ASSERT_EQ(kCommandLogLevelError, eval("${Log.levelValue('error')}").asInt()); + ASSERT_EQ(kCommandLogLevelCritical, eval("${Log.levelValue('critical')}").asInt()); + + ASSERT_EQ("debug", eval("${Log.levelName(Log.DEBUG)}").asString()); + ASSERT_EQ("info", eval("${Log.levelName(Log.INFO)}").asString()); + ASSERT_EQ("warn", eval("${Log.levelName(Log.WARN)}").asString()); + ASSERT_EQ("error", eval("${Log.levelName(Log.ERROR)}").asString()); + ASSERT_EQ("critical", eval("${Log.levelName(Log.CRITICAL)}").asString()); + + ASSERT_EQ(Object::NULL_OBJECT(), eval("${Log.levelValue()}")); + ASSERT_EQ(Object::NULL_OBJECT(), eval("${Log.levelValue('whatever')}")); + ASSERT_EQ(Object::NULL_OBJECT(), eval("${Log.levelValue('info', 'extra param')}")); + ASSERT_EQ(Object::NULL_OBJECT(), eval("${Log.levelName()}")); + ASSERT_EQ(Object::NULL_OBJECT(), eval("${Log.levelName(-1)}")); + ASSERT_EQ(Object::NULL_OBJECT(), eval("${Log.levelName(Log.INFO, 'extra param')}")); + ASSERT_EQ(Object::NULL_OBJECT(), eval("${Log.levelName('info', 'extra param')}")); + ASSERT_EQ("info", eval("${Log.levelName(Log.DEBUG + 1)}").asString()); +} diff --git a/aplcore/unit/datasource/unittest_dynamicindexlistupdate.cpp b/aplcore/unit/datasource/unittest_dynamicindexlistupdate.cpp index 29ca7f7..b47b06b 100644 --- a/aplcore/unit/datasource/unittest_dynamicindexlistupdate.cpp +++ b/aplcore/unit/datasource/unittest_dynamicindexlistupdate.cpp @@ -1326,7 +1326,7 @@ TEST_F(DynamicIndexListUpdateTest, CurrentOrTargetPageCanBeDeleted) // Now delete the target page ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 12))); // And also manually try to go to page 3 - PagerComponent::setPageUtil(context, component, 3, kPageDirectionForward, ActionRef(nullptr)); + PagerComponent::setPageUtil(component, 3, kPageDirectionForward, ActionRef(nullptr)); advanceTime(1000); // We succeed in reaching what was formally page 3 (now page 2) ASSERT_TRUE(CheckPager(2, {{"frame-10"}, {"frame-13"}, {"frame-15"}})); @@ -1345,7 +1345,7 @@ TEST_F(DynamicIndexListUpdateTest, CurrentOrTargetPageCanBeDeleted) // Now delete the source page ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 12))); // And also manually try to go to the last page - PagerComponent::setPageUtil(context, component, 4, kPageDirectionForward, ActionRef(nullptr)); + PagerComponent::setPageUtil(component, 4, kPageDirectionForward, ActionRef(nullptr)); advanceTime(1000); // We succeed in reaching the last page ASSERT_TRUE(CheckPager(3, {{"frame-10"}, {"frame-13"}, {"frame-88"}, {"frame-99"}})); @@ -1420,7 +1420,7 @@ TEST_F(DynamicIndexListUpdateTest, VisualHashRecalculatedOnDynamicDataUpdate) { ASSERT_EQ(2, component->getChildCount()); ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 1}, true)); // Same hash reused, as it should - ASSERT_EQ(3, spyTextMeasure->visualHashes.size()); + ASSERT_EQ(1, spyTextMeasure->visualHashes.size()); auto currentTopItem = root->findComponentById("10"); auto currentTopItemVisualHash = currentTopItem->getCalculated(kPropertyVisualHash).asString(); diff --git a/aplcore/unit/embed/unittest_documentcreate.cpp b/aplcore/unit/embed/unittest_documentcreate.cpp index 6471396..a1aaf5e 100644 --- a/aplcore/unit/embed/unittest_documentcreate.cpp +++ b/aplcore/unit/embed/unittest_documentcreate.cpp @@ -266,6 +266,51 @@ TEST_F(DocumentCreateTest, TestEnvironmentCreationWithIneffectiveOverrides) ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowVideo), true); } +static const char* CUSTOM_ENVIRONMENT = R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "item": { + "type": "Host", + "id": "hostComponent", + "environment": { + "customEnvironmentString": "veryCustom", + "customEnvironmentBool": true, + "customEnvironmentNumber": 42, + "rotated": true, + "aplVersion": "2000", + "customEnvironmentStringEvaluated": "${environment.customEnvironmentString}" + }, + "source": "embeddedDocumentUrl" + } + } + } +})"; + +TEST_F(DocumentCreateTest, CustomEnvironment) +{ + config->setEnvironmentValue("customEnvironmentString", "veryCustom"); + + loadDocument(CUSTOM_ENVIRONMENT); + + // Inflate and verify the embedded document + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + auto embeddedTop = CoreComponent::cast(CoreDocumentContext::cast(embeddedDoc)->topComponent()); + + auto embeddedEnvironment = embeddedTop->getRootConfig().getEnvironmentValues(); + ASSERT_EQ(embeddedEnvironment.at("customEnvironmentString"), "veryCustom"); + ASSERT_EQ(embeddedEnvironment.at("customEnvironmentBool"), true); + ASSERT_EQ(embeddedEnvironment.at("customEnvironmentNumber"), 42); + ASSERT_FALSE(embeddedEnvironment.count("rotated")); + ASSERT_FALSE(embeddedEnvironment.count("aplVersion")); + ASSERT_EQ(embeddedEnvironment.at("customEnvironmentStringEvaluated").asString(), "veryCustom"); +} + TEST_F(DocumentCreateTest, TestRootConfigCreation) { auto dpi = Metrics::CORE_DPI; diff --git a/aplcore/unit/embed/unittest_embedded_extensions.cpp b/aplcore/unit/embed/unittest_embedded_extensions.cpp index d95574f..fbab263 100644 --- a/aplcore/unit/embed/unittest_embedded_extensions.cpp +++ b/aplcore/unit/embed/unittest_embedded_extensions.cpp @@ -80,7 +80,7 @@ class EmbeddedExtensionsTest : public DocumentWrapper { ensureRequestedExtensions(content->getExtensionRequests()); // load them into config via the mediator - mediator->loadExtensions(config, content); + mediator->loadExtensions(config->getExtensionFlags(), content); } void ensureRequestedExtensions(std::set requestedExtensions) { diff --git a/aplcore/unit/embed/unittest_embedded_lifecycle.cpp b/aplcore/unit/embed/unittest_embedded_lifecycle.cpp index 6205e95..6d3f982 100644 --- a/aplcore/unit/embed/unittest_embedded_lifecycle.cpp +++ b/aplcore/unit/embed/unittest_embedded_lifecycle.cpp @@ -84,7 +84,7 @@ static const char* EMBEDDED_DOC = R"({ static const char* PSEUDO_LOG_COMMAND = R"apl([ { - "type": "Log" + "type": "PseudoLog" } ])apl"; @@ -442,7 +442,11 @@ const static char *PARENT_VC = R"({ "HOST" ], "tags": { - "focused": false + "focused": false, + "embedded": { + "attached": false, + "source": "embeddedDocumentUrl" + } }, "id": "hostComponent", "uid": "HOSTID", @@ -534,7 +538,11 @@ const static char *FULL_VC = R"({ "HOST" ], "tags": { - "focused": false + "focused": false, + "embedded": { + "attached": true, + "source": "embeddedDocumentUrl" + } }, "id": "hostComponent", "uid": "HOSTID", @@ -685,11 +693,11 @@ TEST_F(EmbeddedLifecycleTest, ContentAndSourceReuse) auto content = Content::create(EMBEDDED_DOC, session); ASSERT_TRUE(content->isReady()); - auto embeddedDocumentContext1 = documentManager->succeed("embeddedDocumentUrl", content, true, std::make_shared(), true); + auto embeddedDocumentContext1 = documentManager->succeed("embeddedDocumentUrl", content, true, DocumentConfig::create(), true); ASSERT_TRUE(embeddedDocumentContext1); ASSERT_TRUE(CheckSendEvent(root, "LOADED1")); - auto embeddedDocumentContext2 = documentManager->succeed("embeddedDocumentUrl", content, true, std::make_shared(), true); + auto embeddedDocumentContext2 = documentManager->succeed("embeddedDocumentUrl", content, true, DocumentConfig::create(), true); ASSERT_TRUE(embeddedDocumentContext2); ASSERT_TRUE(CheckSendEvent(root, "LOADED2")); } @@ -727,7 +735,712 @@ TEST_F(EmbeddedLifecycleTest, SingleHost) auto content = Content::create(EMBEDDED_DOC, session); ASSERT_TRUE(content->isReady()); - auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, std::make_shared(), true); + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, DocumentConfig::create(), true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); +} + +TEST_F(EmbeddedLifecycleTest, ChangeSourceAfterDocumentLoaded) +{ + loadDocument(HOST_DOC); + + // Host component has no children at the beginning + auto host = component->getCoreChildAt(0); + ASSERT_EQ(0, host->getChildCount()); + + auto content = Content::create(EMBEDDED_DOC, session); + ASSERT_TRUE(content->isReady()); + + auto embeddedDocument = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocument); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Now there is one child (due to embedded document Text component) + ASSERT_EQ(1, host->getChildCount()); + + auto text = root->findComponentById("embeddedText"); + ASSERT_EQ("Hello, World!", text->getCalculated(kPropertyText).asString()); + + // Change the source to something else + auto action = executeCommand( + rootDocument, + "SetValue", { + {"componentId", "hostComponent"}, + {"property", "source"}, + {"value", "anotherEmbeddedDocumentUrl"} + }, false); + + // Back to no children (Host is empty) + ASSERT_EQ(0, host->getChildCount()); + + auto embeddedDocument2 = documentManager->succeed("anotherEmbeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocument2); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Again there is one child (due to embedded document Text component) + ASSERT_EQ(1, host->getChildCount()); +} + +TEST_F(EmbeddedLifecycleTest, ChangeSourceBeforeDocumentLoaded) { + loadDocument(HOST_DOC); + + // Host component has no children at the beginning + auto host = component->getCoreChildAt(0); + ASSERT_EQ(0, host->getChildCount()); + + // Change the source to something else + auto action = executeCommand(rootDocument, "SetValue", + {{"componentId", "hostComponent"}, + {"property", "source"}, + {"value", "anotherEmbeddedDocumentUrl"}}, + false); + + auto content = Content::create(EMBEDDED_DOC, session); + ASSERT_TRUE(content->isReady()); + + // Original request is no longer needed + auto embeddedDocument = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_FALSE(embeddedDocument); + ASSERT_FALSE(CheckSendEvent(root, "LOADED")); + + // Still no children + ASSERT_EQ(0, host->getChildCount()); + auto text = root->findComponentById("embeddedText"); + ASSERT_FALSE(text); + + auto embeddedDocument2 = documentManager->succeed("anotherEmbeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocument2); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Now there's one child (due to embedded document Text component) + ASSERT_EQ(1, host->getChildCount()); + text = root->findComponentById("embeddedText"); + ASSERT_TRUE(text); + ASSERT_EQ("Hello, World!", text->getCalculated(kPropertyText).asString()); +} + +static const char* CUSTOM_EMBEDDED_ENV = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${environment.magic}" + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, CustomEnv) +{ + // Host document inflates + loadDocument(SINGLE_HOST_DOC); + + auto content = Content::create(CUSTOM_EMBEDDED_ENV, session); + ASSERT_TRUE(content->isReady()); + + auto documentConfig = DocumentConfig::create(); + documentConfig->setEnvironmentValue("magic", "Very magic."); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, documentConfig, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_EQ("Very magic.", root->findComponentById("embeddedText")->getCalculated(apl::kPropertyText).asString()); +} + +static const char* HOST_DOC_AUTO = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "item": { + "type": "Host", + "width": "auto", + "height": "auto", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + } + } + } +})"; + +static const char* FIXED_EMBEDDED_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "width": 300, + "height": 300, + "id": "embeddedText", + "text": "Hello, World!", + "entities": "EMBEDDED" + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, AutoSizedEmbedded) +{ + loadDocument(HOST_DOC_AUTO); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(FIXED_EMBEDDED_DOC, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 300, 300)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 300, 300)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); + + // Change size directly + auto action = executeCommand( + embeddedDocumentContext, + "SetValue", { + {"componentId", "embeddedText"}, + {"property", "width"}, + {"value", 200} + }, false); + + advanceTime(100); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 200, 300)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 200, 300)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); +} + +static const char* AUTO_EMBEDDED_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "width": "auto", + "height": "auto", + "id": "embeddedText", + "text": "Hello, World!" + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, AutoSizedAutoEmbedded) +{ + loadDocument(HOST_DOC_AUTO); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(AUTO_EMBEDDED_DOC, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 130, 10)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 130, 10)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); + + + executeCommand( + embeddedDocumentContext, + "SetValue", { + {"componentId", "embeddedText"}, + {"property", "text"}, + {"value", "Hello, World! Maybe, not sure yet."} + }, false); + + advanceTime(100); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 340, 10)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 340, 10)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); +} + +static const char* HOST_DOC_AUTO_MINMAX_WIDTH = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "item": { + "type": "Host", + "width": "auto", + "minWidth": 100, + "maxWidth": 200, + "height": "auto", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + } + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, AutoSizedEmbeddedMinMaxWidth) +{ + loadDocument(HOST_DOC_AUTO_MINMAX_WIDTH); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(AUTO_EMBEDDED_DOC, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 130, 10)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 130, 10)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); + + + executeCommand( + embeddedDocumentContext, + "SetValue", { + {"componentId", "embeddedText"}, + {"property", "text"}, + {"value", "Hello"} + }, false); + + advanceTime(100); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 100, 10)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 100, 10)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); + + + executeCommand( + embeddedDocumentContext, + "SetValue", { + {"componentId", "embeddedText"}, + {"property", "text"}, + {"value", "Hello, World! Maybe, not sure yet."} + }, false); + + advanceTime(100); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 200, 20)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 200, 20)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); +} + +static const char* HOST_DOC_AUTO_MINMAX_HEIGHT = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "item": { + "type": "Host", + "width": 50, + "height": "auto", + "minHeight": 20, + "maxHeight": 60, + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + } + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, AutoSizedEmbeddedMinMaxHeight) +{ + loadDocument(HOST_DOC_AUTO_MINMAX_HEIGHT); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(AUTO_EMBEDDED_DOC, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 50, 30)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 50, 30)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); + + + executeCommand( + embeddedDocumentContext, + "SetValue", { + {"componentId", "embeddedText"}, + {"property", "text"}, + {"value", "Hello"} + }, false); + + advanceTime(100); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 50, 20)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 50, 20)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); + + + executeCommand( + embeddedDocumentContext, + "SetValue", { + {"componentId", "embeddedText"}, + {"property", "text"}, + {"value", "Hello, World! Maybe, not sure yet."} + }, false); + + advanceTime(100); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 50, 60)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 50, 60)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); +} + +static const char* HOST_DOC_AUTO_MINMAX = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "item": { + "type": "Host", + "width": "auto", + "height": "auto", + "minWidth": 60, + "maxWidth": 150, + "minHeight": 20, + "maxHeight": 25, + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + } + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, AutoSizedEmbeddedMinMax) +{ + loadDocument(HOST_DOC_AUTO_MINMAX); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(AUTO_EMBEDDED_DOC, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); ASSERT_TRUE(embeddedDocumentContext); ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 130, 20)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 130, 20)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); + + + executeCommand( + embeddedDocumentContext, + "SetValue", { + {"componentId", "embeddedText"}, + {"property", "text"}, + {"value", "Hello"} + }, false); + + advanceTime(100); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 60, 20)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 60, 20)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); + + + executeCommand( + embeddedDocumentContext, + "SetValue", { + {"componentId", "embeddedText"}, + {"property", "text"}, + {"value", "Hello, World! Maybe, not sure yet."} + }, false); + + advanceTime(100); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 150, 25)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 150, 25)); + ASSERT_TRUE(CheckComponent(component, 1024, 800)); + ASSERT_TRUE(CheckViewport(root, 1024, 800)); +} + +static const char* HOST_AUTO_DOC_AUTO = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Container", + "width": "auto", + "height": "auto", + "item": { + "type": "Host", + "width": "auto", + "height": "auto", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + } + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, AutoSizedAutoEmbeddedAutoHost) +{ + metrics = Metrics().size(100, 100).minAndMaxHeight(50, 100).minAndMaxWidth(100, 500); + loadDocument(HOST_AUTO_DOC_AUTO); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(AUTO_EMBEDDED_DOC, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 130, 10)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 130, 10)); + ASSERT_TRUE(CheckComponent(component, 130, 50)); + ASSERT_TRUE(CheckViewport(root, 130, 50)); + + + executeCommand( + embeddedDocumentContext, + "SetValue", { + {"componentId", "embeddedText"}, + {"property", "text"}, + {"value", "Hello, World! Maybe, not sure yet."} + }, false); + + advanceTime(100); + + ASSERT_TRUE(CheckComponent(root->findComponentById("embeddedText"), 340, 10)); + ASSERT_TRUE(CheckComponent(root->findComponentById("hostComponent"), 340, 10)); + ASSERT_TRUE(CheckComponent(component, 340, 50)); + ASSERT_TRUE(CheckViewport(root, 340, 50)); +} + +static const char* SCROLLABLE_MULTI_HOST = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "item": { + "type": "Sequence", + "width": "auto", + "height": 100, + "data": [ + "Hello first time.", + "Hello very second time. For real. Not kidding now.", + "Hello third time time.", + "Bye now" + ], + "item": { + "type": "Host", + "width": "auto", + "height": "auto", + "entities": "HOST", + "minWidth": 100, + "maxWidth": 200, + "source": "embeddedDocumentUrl${index}", + "Input": "${data}", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + } + } + } +})"; + +static const char* PARAMETERIZED_EMBEDDED_TEXT = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "parameters": [ "Input" ], + "item": { + "type": "Text", + "width": "auto", + "height": "auto", + "text": "${Input}" + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, ComplexScrollable) +{ + metrics = Metrics().size(100, 100).minAndMaxHeight(50, 200).minAndMaxWidth(50, 500); + loadDocument(SCROLLABLE_MULTI_HOST); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto embeddedContent = Content::create(PARAMETERIZED_EMBEDDED_TEXT, embeddedSession); + + auto requestWeak = documentManager->get("embeddedDocumentUrl0"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl0"); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl0", embeddedContent, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + + requestWeak = documentManager->get("embeddedDocumentUrl1"); + ASSERT_TRUE(requestWeak.lock()); + request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl1"); + + embeddedContent = Content::create(PARAMETERIZED_EMBEDDED_TEXT, embeddedSession); + embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl1", embeddedContent, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + + requestWeak = documentManager->get("embeddedDocumentUrl2"); + ASSERT_TRUE(requestWeak.lock()); + request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl2"); + + embeddedContent = Content::create(PARAMETERIZED_EMBEDDED_TEXT, embeddedSession); + embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl2", embeddedContent, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + + requestWeak = documentManager->get("embeddedDocumentUrl3"); + ASSERT_TRUE(requestWeak.lock()); + request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl3"); + + embeddedContent = Content::create(PARAMETERIZED_EMBEDDED_TEXT, embeddedSession); + embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl3", embeddedContent, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + + ASSERT_TRUE(CheckComponent(component, 200, 100)); + ASSERT_TRUE(CheckViewport(root, 200, 100)); + + ASSERT_TRUE(CheckComponent(component->getCoreChildAt(0)->getCoreChildAt(0), 170, 10)); + ASSERT_TRUE(CheckComponent(component->getCoreChildAt(1)->getCoreChildAt(0), 200, 30)); + ASSERT_TRUE(CheckComponent(component->getCoreChildAt(2)->getCoreChildAt(0), 200, 20)); + ASSERT_TRUE(CheckComponent(component->getCoreChildAt(3)->getCoreChildAt(0), 100, 10)); } diff --git a/aplcore/unit/embed/unittest_embedded_reinflate.cpp b/aplcore/unit/embed/unittest_embedded_reinflate.cpp index 4b6ab40..951cb79 100644 --- a/aplcore/unit/embed/unittest_embedded_reinflate.cpp +++ b/aplcore/unit/embed/unittest_embedded_reinflate.cpp @@ -52,8 +52,8 @@ static const char* HOST_DOC_CONFIG_CHANGE = R"({ "items": [ { "type": "Host", - "width": "100%", - "height": "100%", + "width": "80%", + "height": "80%", "id": "hostComponent", "entities": "HOST", "source": "embeddedDocumentUrl", @@ -178,6 +178,8 @@ static const char* EMBEDDED_DOC_REINFLATE = R"({ }, "mainTemplate": { "item": { + "height": "100%", + "width": "100%", "type": "Text", "id": "embeddedText", "text": "${viewport.theme}", @@ -248,7 +250,7 @@ TEST_F(EmbeddedReinflateTest, ConfigChangeSize) ASSERT_TRUE(CheckSendEvent(root, 500, 500, "dark", "hub", 1, "normal", false, true, false)); advanceTime(100); - ASSERT_TRUE(CheckSendEvent(root, 500, 500, "dark", "hub", 1, "normal", false, true, false)); + ASSERT_TRUE(CheckSendEvent(root, 400, 400, "dark", "hub", 1, "normal", false, true, false)); } // Size change without config change causes one only in Embedded document @@ -276,7 +278,7 @@ TEST_F(EmbeddedReinflateTest, DirectChangeSize) ASSERT_FALSE(root->hasEvent()); advanceTime(100); - ASSERT_TRUE(CheckSendEvent(root, 300, 400, "dark", "hub", 1, "normal", false, true, false)); + ASSERT_TRUE(CheckSendEvent(root, 300, 320, "dark", "hub", 1, "normal", false, true, false)); } // Relevant config change passed over to the embedded doc @@ -352,6 +354,66 @@ TEST_F(EmbeddedReinflateTest, ConfigChangeThemeEmbedded) ASSERT_EQ("light", embeddedText->getCalculated(kPropertyText).asString()); } +// Size change or environment ConfigChange shouldn't lead to reinflate of embedded doc +TEST_F(EmbeddedReinflateTest, ConfigChangeSizeEmbeddedNope) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_CONFIG_CHANGE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_REINFLATE, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto embed = CoreComponent::cast(root->findComponentById("embeddedText")); + auto actualBounds = embed->getCalculated(apl::kPropertyBounds).get(); + ASSERT_EQ(Rect(0, 0, 320, 320), actualBounds) << "Actual: " << actualBounds.toString(); + + auto configChange = ConfigurationChange(500, 500); + root->configurationChange(configChange); + ASSERT_TRUE(CheckSendEvent(root, 500, 500, "dark", "hub", 1, "normal", false, true, false)); + + advanceTime(100); + root->clearPending(); + embed = CoreComponent::cast(root->findComponentById("embeddedText")); + actualBounds = embed->getCalculated(apl::kPropertyBounds).get(); + ASSERT_EQ(Rect(0, 0, 400, 400), actualBounds) << "Actual: " << actualBounds.toString(); +} + +// Size change or environment ConfigChange shouldn't lead to reinflate of embedded doc +TEST_F(EmbeddedReinflateTest, ConfigChangeEnvEmbeddedNope) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_CONFIG_CHANGE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_CONFIG, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto configChange = ConfigurationChange().environmentValue("someEnvironment", true); + root->configurationChange(configChange); + ASSERT_TRUE(CheckSendEvent(root, 100, 100, "dark", "hub", 1, "normal", false, false, false)); + + advanceTime(100); + ASSERT_FALSE(root->hasEvent()); +} // Config change may lead to Embedded document reinflate TEST_F(EmbeddedReinflateTest, ConfigChangeThemeHostNonResolved) @@ -536,4 +598,105 @@ TEST_F(EmbeddedReinflateTest, ConfigChangeThemeHostNoPreserve) ASSERT_EQ("light", hostText->getCalculated(kPropertyText).asString()); embeddedText = root->findComponentById("embeddedText"); ASSERT_EQ("light", embeddedText->getCalculated(kPropertyText).asString()); +} + +static const char* HOST_DOC_ENVIRONMENT_PASS = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ], + "bind": [ + { "name": "BoundEnvPasser", "value": "${environment.magic}" } + ], + "environment": { + "BoundEnv": "${BoundEnvPasser}", + "Magic": "${environment.magic}", + "ViewportEnv": "${viewport.mode}", + "Reason": "${environment.reason}", + "ScreenMode": "${environment.screenMode}", + "FontScale": "${environment.fontScale}", + "ScreenReader": "${environment.screenReader}", + "DisallowVideo": "${environment.disallowVideo}" + } + } + } +})"; + +static const char* EMBEDDED_DOC_CONFIG_ENVIRONMENT = R"({ + "type": "APL", + "version": "2023.2", + "onConfigChange": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER_EMBEDDED", + "delay": 100, + "arguments": [ + "${event.environment.BoundEnv}", "${event.environment.DisallowVideo}", + "${event.environment.FontScale}", "${event.environment.Magic}", + "${event.environment.Reason}", "${event.environment.ScreenMode}", + "${event.environment.ScreenReader}", "${event.environment.ViewportEnv}" + ] + }, + { + "type": "Reinflate", + "sequencer": "REINFLATE_EMBEDDED", + "delay": 200 + } + ], + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${environment.BoundEnv} ${environment.DisallowVideo} ${environment.FontScale} ${environment.Magic} ${environment.Reason} ${environment.ScreenMode} ${environment.ScreenReader} ${environment.ViewportEnv}" + } + } +})"; + +TEST_F(EmbeddedReinflateTest, EnvironmentPassing) +{ + metrics.size(400, 400); + config->setEnvironmentValue("magic", false); + + loadDocument(HOST_DOC_ENVIRONMENT_PASS); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_CONFIG_ENVIRONMENT, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto textComp = root->findComponentById("embeddedText"); + ASSERT_EQ("false false 1 false initial normal false hub", textComp->getCalculated(apl::kPropertyText).asString()); + + auto configChange = ConfigurationChange() + .environmentValue("magic", true) + .mode(ViewportMode::kViewportModeMobile) + .screenMode(RootConfig::ScreenMode::kScreenModeHighContrast) + .fontScale(54) + .screenReader(true) + .disallowVideo(true); + root->configurationChange(configChange); + + advanceTime(100); + ASSERT_TRUE(CheckSendEvent(root, false, true, 54.000000, true, "reinflation", 1, true, "mobile")); + + advanceTime(100); + textComp = root->findComponentById("embeddedText"); + ASSERT_EQ("false true 54 true reinflation 1 true mobile", textComp->getCalculated(apl::kPropertyText).asString()); } \ No newline at end of file diff --git a/aplcore/unit/engine/unittest_builder.cpp b/aplcore/unit/engine/unittest_builder.cpp index 8af1333..8ff9cbc 100644 --- a/aplcore/unit/engine/unittest_builder.cpp +++ b/aplcore/unit/engine/unittest_builder.cpp @@ -1118,7 +1118,7 @@ TEST_F(BuilderTest, SimpleScrollView) // Standard properties ASSERT_EQ("", component->getCalculated(kPropertyAccessibilityLabel).getString()); - ASSERT_EQ(Object::EMPTY_ARRAY(), component->getCalculated(kPropertyAccessibilityActions)); + ASSERT_EQ(2, component->getCalculated(kPropertyAccessibilityActions).size()); ASSERT_EQ(Object::FALSE_OBJECT(), component->getCalculated(kPropertyDisabled)); ASSERT_EQ(Object(Dimension(100)), component->getCalculated(kPropertyHeight)); ASSERT_EQ(Object::NULL_OBJECT(), component->getCalculated(kPropertyMaxHeight)); @@ -3265,7 +3265,7 @@ commands": [ */ TEST_F(BuilderTest, NullLayoutReturnsNullPointer) { - auto content = apl::Content::create(NULL_LAYOUT_NULL_POINTER); + auto content = apl::Content::create(NULL_LAYOUT_NULL_POINTER, session); EXPECT_TRUE(content != nullptr); EXPECT_FALSE(apl::RootContext::create(apl::Metrics() .size(1280, 800) @@ -3273,4 +3273,5 @@ TEST_F(BuilderTest, NullLayoutReturnsNullPointer) .shape(apl::ScreenShape::ROUND), content)); + ASSERT_TRUE(session->checkAndClear()); } \ No newline at end of file diff --git a/aplcore/unit/engine/unittest_builder_pager.cpp b/aplcore/unit/engine/unittest_builder_pager.cpp index 8a1d80a..54c85d9 100644 --- a/aplcore/unit/engine/unittest_builder_pager.cpp +++ b/aplcore/unit/engine/unittest_builder_pager.cpp @@ -71,7 +71,7 @@ TEST_F(BuilderTestPager, SimplePager) // Standard properties ASSERT_TRUE(IsEqual("", component->getCalculated(kPropertyAccessibilityLabel))); - ASSERT_EQ(Object::EMPTY_ARRAY(), component->getCalculated(kPropertyAccessibilityActions)); + ASSERT_EQ(2, component->getCalculated(kPropertyAccessibilityActions).size()); ASSERT_TRUE(IsEqual(Object::FALSE_OBJECT(), component->getCalculated(kPropertyDisabled))); ASSERT_TRUE(IsEqual(Dimension(200), component->getCalculated(kPropertyHeight))); ASSERT_TRUE(IsEqual(Object::NULL_OBJECT(), component->getCalculated(kPropertyMaxHeight))); diff --git a/aplcore/unit/engine/unittest_builder_preserve.cpp b/aplcore/unit/engine/unittest_builder_preserve.cpp index 084cb18..4cd8bba 100644 --- a/aplcore/unit/engine/unittest_builder_preserve.cpp +++ b/aplcore/unit/engine/unittest_builder_preserve.cpp @@ -700,7 +700,7 @@ TEST_F(BuilderPreserveTest, PagerChangePages) ASSERT_EQ(3, currentPage); ASSERT_TRUE(IsEqual("Belgian Sheepdog=3", component->getChildAt(currentPage)->getCalculated(kPropertyText).asString())); - ASSERT_TRUE(CheckDirty(component, kPropertyCurrentPage)); + ASSERT_TRUE(CheckDirty(component, kPropertyCurrentPage, kPropertyNotifyChildrenChanged)); ASSERT_TRUE(CheckDirty(root, component)); ASSERT_TRUE(root->isVisualContextDirty()); root->clearVisualContextDirty(); @@ -712,7 +712,7 @@ TEST_F(BuilderPreserveTest, PagerChangePages) ASSERT_EQ(2, currentPage); ASSERT_TRUE(IsEqual("Golden Retriever=2", component->getChildAt(currentPage)->getCalculated(kPropertyText).asString())); - ASSERT_TRUE(CheckDirty(component, kPropertyCurrentPage)); + ASSERT_TRUE(CheckDirty(component, kPropertyCurrentPage, kPropertyNotifyChildrenChanged)); ASSERT_TRUE(CheckDirty(root, component)); ASSERT_TRUE(root->isVisualContextDirty()); root->clearVisualContextDirty(); @@ -722,7 +722,7 @@ TEST_F(BuilderPreserveTest, PagerChangePages) currentPage = component->getCalculated(kPropertyCurrentPage).asInt(); ASSERT_EQ(1, currentPage); ASSERT_TRUE(IsEqual("Chinook=1", component->getChildAt(currentPage)->getCalculated(kPropertyText).asString())); - ASSERT_TRUE(CheckDirty(component, kPropertyCurrentPage)); + ASSERT_TRUE(CheckDirty(component, kPropertyCurrentPage, kPropertyNotifyChildrenChanged)); ASSERT_TRUE(CheckDirty(root, component)); ASSERT_TRUE(root->isVisualContextDirty()); } diff --git a/aplcore/unit/engine/unittest_builder_sequence.cpp b/aplcore/unit/engine/unittest_builder_sequence.cpp index c5d0d66..0d9ae1e 100644 --- a/aplcore/unit/engine/unittest_builder_sequence.cpp +++ b/aplcore/unit/engine/unittest_builder_sequence.cpp @@ -49,7 +49,7 @@ TEST_F(BuilderTestSequence, Simple) // Standard properties ASSERT_EQ("", component->getCalculated(kPropertyAccessibilityLabel).getString()); - ASSERT_EQ(Object::EMPTY_ARRAY(), component->getCalculated(kPropertyAccessibilityActions)); + ASSERT_EQ(2, component->getCalculated(kPropertyAccessibilityActions).size()); ASSERT_EQ(Object::FALSE_OBJECT(), component->getCalculated(kPropertyDisabled)); ASSERT_EQ(Object(Dimension(100)), component->getCalculated(kPropertyHeight)); ASSERT_EQ(Object::NULL_OBJECT(), component->getCalculated(kPropertyMaxHeight)); @@ -172,7 +172,7 @@ TEST_F(BuilderTestSequence, Empty) // Standard properties ASSERT_EQ("", component->getCalculated(kPropertyAccessibilityLabel).getString()); - ASSERT_EQ(Object::EMPTY_ARRAY(), component->getCalculated(kPropertyAccessibilityActions)); + ASSERT_EQ(2, component->getCalculated(kPropertyAccessibilityActions).size()); ASSERT_EQ(Object::FALSE_OBJECT(), component->getCalculated(kPropertyDisabled)); ASSERT_EQ(Object(Dimension(100)), component->getCalculated(kPropertyHeight)); ASSERT_EQ(Object::NULL_OBJECT(), component->getCalculated(kPropertyMaxHeight)); @@ -821,4 +821,56 @@ TEST_F(BuilderTestSequence, SequenceInflationTestHorizontalLTR) ASSERT_TRUE(CheckChildrenLaidOut(component, Range(32, 90), true)); ASSERT_TRUE(CheckChildrenLaidOut(component, Range(91, 99), false)); +} + +const char *AUTO_SIZE_TEXT_CHILD = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Container", + "height": 800, + "width": 800, + "items": [ + { + "type": "Sequence", + "height": 20, + "width": 50, + "items": { + "type": "Text", + "width": "auto", + "height": "auto", + "text": "text text text text text text text text" + } + }, + { + "type": "ScrollView", + "height": 20, + "width": 50, + "items": { + "type": "Text", + "width": "auto", + "height": "auto", + "text": "text text text text text text text text" + } + } + ] + } + } +})"; + +TEST_F(BuilderTestSequence, AutoSizeTextChild) +{ + loadDocument(AUTO_SIZE_TEXT_CHILD); + + ASSERT_TRUE(component); + + ASSERT_EQ(Rect(0, 0, 50, 20), component->getChildAt(0)->getCalculated(apl::kPropertyBounds).get()); + auto textBounds = component->getChildAt(0)->getChildAt(0)->getCalculated(apl::kPropertyBounds).get(); + ASSERT_EQ(Rect(0, 0, 50, 80), textBounds); + + ASSERT_EQ(Rect(0, 20, 50, 20), component->getChildAt(1)->getCalculated(apl::kPropertyBounds).get()); + textBounds = component->getChildAt(1)->getChildAt(0)->getCalculated(apl::kPropertyBounds).get(); + ASSERT_EQ(Rect(0, 0, 50, 80), textBounds); } \ No newline at end of file diff --git a/aplcore/unit/engine/unittest_context.cpp b/aplcore/unit/engine/unittest_context.cpp index abe9a80..26932fc 100644 --- a/aplcore/unit/engine/unittest_context.cpp +++ b/aplcore/unit/engine/unittest_context.cpp @@ -27,8 +27,8 @@ class ContextTest : public MemoryWrapper { .dpi(320) .theme("green") .shape(apl::ROUND) - .autoSizeWidth(true) - .autoSizeHeight(true) + .minAndMaxWidth(1024, 3072) + .minAndMaxHeight(1800, 2200) .mode(apl::kViewportModeTV); auto r = RootConfig().set(RootProperty::kAgentName, "UnitTests"); r.setEnvironmentValue("testEnvironment", "23.2"); @@ -51,7 +51,7 @@ TEST_F(ContextTest, Basic) EXPECT_EQ("1.0", env.get("agentVersion").asString()); EXPECT_EQ("normal", env.get("animation").asString()); EXPECT_FALSE(env.get("allowOpenURL").asBoolean()); - EXPECT_EQ("2023.2", env.get("aplVersion").asString()); + EXPECT_EQ("2023.3", env.get("aplVersion").asString()); EXPECT_FALSE(env.get("disallowDialog").asBoolean()); EXPECT_FALSE(env.get("disallowEditText").asBoolean()); EXPECT_FALSE(env.get("disallowVideo").asBoolean()); @@ -61,7 +61,7 @@ TEST_F(ContextTest, Basic) EXPECT_EQ("", env.get("lang").asString()); EXPECT_EQ("LTR", env.get("layoutDirection").asString()); EXPECT_EQ(false, env.get("screenReader").asBoolean()); - EXPECT_EQ("2023.2", env.get("documentAPLVersion").asString()); + EXPECT_EQ("2023.3", env.get("documentAPLVersion").asString()); auto timing = env.get("timing"); EXPECT_EQ(500, timing.get("doublePressTimeout").asNumber()); @@ -82,6 +82,10 @@ TEST_F(ContextTest, Basic) EXPECT_EQ(Object("tv"), viewport.get("mode")); EXPECT_EQ(true, viewport.get("autoWidth").asBoolean()); EXPECT_EQ(true, viewport.get("autoHeight").asBoolean()); + EXPECT_EQ(512, viewport.get("minWidth").asNumber()); + EXPECT_EQ(1536, viewport.get("maxWidth").asNumber()); + EXPECT_EQ(900, viewport.get("minHeight").asNumber()); + EXPECT_EQ(1100, viewport.get("maxHeight").asNumber()); EXPECT_TRUE(env.has("extension")); @@ -109,7 +113,7 @@ TEST_F(ContextTest, Evaluation) EXPECT_EQ("1.0", env.get("agentVersion").asString()); EXPECT_EQ("normal", env.get("animation").asString()); EXPECT_FALSE(env.get("allowOpenURL").asBoolean()); - EXPECT_EQ("2023.2", env.get("aplVersion").asString()); + EXPECT_EQ("2023.3", env.get("aplVersion").asString()); EXPECT_FALSE(env.get("disallowDialog").asBoolean()); EXPECT_FALSE(env.get("disallowEditText").asBoolean()); EXPECT_FALSE(env.get("disallowVideo").asBoolean()); @@ -137,6 +141,12 @@ TEST_F(ContextTest, Evaluation) EXPECT_EQ("rectangle", viewport.get("shape").asString()); EXPECT_EQ("dark", viewport.get("theme").asString()); EXPECT_EQ(Object("hub"), viewport.get("mode")); + EXPECT_EQ(false, viewport.get("autoWidth").asBoolean()); + EXPECT_EQ(false, viewport.get("autoHeight").asBoolean()); + EXPECT_EQ(1024, viewport.get("minWidth").asNumber()); + EXPECT_EQ(1024, viewport.get("maxWidth").asNumber()); + EXPECT_EQ(800, viewport.get("minHeight").asNumber()); + EXPECT_EQ(800, viewport.get("maxHeight").asNumber()); EXPECT_FALSE(env.has("extension")); @@ -315,7 +325,10 @@ TEST_F(ContextTest, UnknownModeString) TEST_F(ContextTest, AutoSize) { auto localTest = [&](bool width, bool height) -> ::testing::AssertionResult { - auto c = Context::createTestContext(Metrics().autoSizeWidth(width).autoSizeHeight(height), session); + Metrics m; + if (width) m.minAndMaxWidth(100, 1000); + if (height) m.minAndMaxHeight(100, 1000); + auto c = Context::createTestContext(m, session); if (width != c->opt("viewport").get("autoWidth").asBoolean()) return ::testing::AssertionFailure() << "Incorrect width"; if (height != c->opt("viewport").get("autoHeight").asBoolean()) @@ -329,6 +342,7 @@ TEST_F(ContextTest, AutoSize) { ASSERT_TRUE(localTest(true, true)); } + static const char * TIME_DOC = "{" " \"type\": \"APL\"," @@ -349,10 +363,10 @@ TEST_F(ContextTest, Time) const long long deltaTime = 3600 * 1000; auto rootConfig = RootConfig().set(RootProperty::kUTCTime, utcTime).set(RootProperty::kLocalTimeAdjustment, deltaTime); - ASSERT_EQ(utcTime, rootConfig.getUTCTime()); - ASSERT_EQ(deltaTime, rootConfig.getLocalTimeAdjustment()); + ASSERT_EQ(utcTime, rootConfig.getProperty(RootProperty::kUTCTime).getDouble()); + ASSERT_EQ(deltaTime, rootConfig.getProperty(RootProperty::kLocalTimeAdjustment).getDouble()); - auto content = Content::create(TIME_DOC); + auto content = Content::create(TIME_DOC, session); auto root = RootContext::create(Metrics(), content, rootConfig); auto component = root->topComponent(); @@ -377,7 +391,7 @@ TEST_F(ContextTest, Time) std::chrono::system_clock::now().time_since_epoch()); rootConfig = RootConfig().set(RootProperty::kUTCTime, now.count()); - ASSERT_EQ(std::chrono::milliseconds{static_cast(rootConfig.getUTCTime())}, now); + ASSERT_EQ(std::chrono::milliseconds{static_cast(rootConfig.getProperty(RootProperty::kUTCTime).getDouble())}, now); component = nullptr; root = nullptr; @@ -399,7 +413,7 @@ static const char * DEFAULT_ENV_DOC = R"apl( TEST_F(ContextTest, DefaultEnv) { auto rootConfig = RootConfig(); - auto content = Content::create(DEFAULT_ENV_DOC); + auto content = Content::create(DEFAULT_ENV_DOC, session); auto root = RootContext::create(Metrics(), content, rootConfig); auto component = root->topComponent(); @@ -427,7 +441,7 @@ static const char * BASIC_ENV_DOC = R"apl( TEST_F(ContextTest, LangAndLayoutDirectionCheck) { auto rootConfig = RootConfig(); - auto content = Content::create(BASIC_ENV_DOC); + auto content = Content::create(BASIC_ENV_DOC, session); auto root = RootContext::create(Metrics(), content, rootConfig); auto component = root->topComponent(); @@ -445,7 +459,7 @@ TEST_F(ContextTest, NoStandardFunction) { auto ctx1 = Context::createTypeEvaluationContext( rootConfig, APLVersion::getDefaultReportedVersionString(), session); - auto ctx2 = Context::createBackgroundEvaluationContext( + auto ctx2 = Context::createContentEvaluationContext( metrics, rootConfig, APLVersion::getDefaultReportedVersionString(), metrics.getTheme(), session); @@ -463,7 +477,7 @@ TEST_F(ContextTest, NoStandardFunction) { TEST_F(ContextTest, TrivialMethodChecks) { auto rootConfig = RootConfig().set(RootProperty::kLang, "de-DE"); - auto content = Content::create(BASIC_ENV_DOC); + auto content = Content::create(BASIC_ENV_DOC, session); auto root = std::static_pointer_cast(RootContext::create(Metrics().theme("dark"), content, rootConfig)); ASSERT_EQ(std::string("de-DE"), root->getRootConfig().getProperty(RootProperty::kLang).asString()); @@ -499,7 +513,7 @@ static const char * OVERRIDE_ENV_DOC = R"apl( TEST_F(ContextTest, OverrideCheck) { auto rootConfig = RootConfig(); - auto content = Content::create(OVERRIDE_ENV_DOC); + auto content = Content::create(OVERRIDE_ENV_DOC, session); auto root = RootContext::create(Metrics(), content, rootConfig); auto component = root->topComponent(); @@ -530,7 +544,7 @@ static const char * CANCEL_OVERRIDE_ENV_DOC = R"apl( TEST_F(ContextTest, CancelOverrideCheck) { auto rootConfig = RootConfig(); - auto content = Content::create(CANCEL_OVERRIDE_ENV_DOC); + auto content = Content::create(CANCEL_OVERRIDE_ENV_DOC, session); auto root = RootContext::create(Metrics(), content, rootConfig); auto component = root->topComponent(); @@ -560,7 +574,7 @@ static const char *ENVIRONMENT_PAYLOAD = R"apl( TEST_F(ContextTest, EnvironmentPayload) { auto rootConfig = RootConfig(); - auto content = Content::create(ENVIRONMENT_PAYLOAD); + auto content = Content::create(ENVIRONMENT_PAYLOAD, session); content->addData("payload", R"({"lang": "en-ES", "layoutDirection": "RTL"})" ); auto root = RootContext::create(Metrics(), content, rootConfig); auto component = root->topComponent(); diff --git a/aplcore/unit/engine/unittest_context_apl_version.cpp b/aplcore/unit/engine/unittest_context_apl_version.cpp index 7fa1b82..b324a26 100644 --- a/aplcore/unit/engine/unittest_context_apl_version.cpp +++ b/aplcore/unit/engine/unittest_context_apl_version.cpp @@ -56,9 +56,9 @@ TEST_F(ContextAPLVersionTest, Basic) loadDocument(BASIC); auto context = component->getContext(); ASSERT_EQ("1.9", context->getRequestedAPLVersion()); - ASSERT_TRUE(IsEqual("2023.2", evaluate(*context, "${environment.aplVersion}"))); + ASSERT_TRUE(IsEqual("2023.3", evaluate(*context, "${environment.aplVersion}"))); ASSERT_TRUE(IsEqual("1.9", evaluate(*context, "${environment.documentAPLVersion}"))); // The document background is evaluated is a special data-binding context ASSERT_TRUE(IsEqual(content->getBackground(Metrics(), RootConfig()), Color(Color::RED))); -} \ No newline at end of file +} diff --git a/aplcore/unit/engine/unittest_layouts.cpp b/aplcore/unit/engine/unittest_layouts.cpp index 8740eb4..15e763a 100644 --- a/aplcore/unit/engine/unittest_layouts.cpp +++ b/aplcore/unit/engine/unittest_layouts.cpp @@ -777,4 +777,44 @@ TEST_F(LayoutTest, BAD_PARAMETER_NAME) loadDocument(BAD_PARAMETER_NAME); ASSERT_TRUE(component); ASSERT_TRUE(ConsoleMessage()); +} + +static const char *SIMPLE_EDIT_TEXT_TOP = R"apl({ + "type": "APL", + "version": "1.4", + "mainTemplate": { + "item": { + "type": "EditText", + "id": "EDITTEXT" + } + } +})apl"; + +static const char *SIMPLE_VIDEO_TOP = R"apl({ + "type": "APL", + "version": "1.4", + "mainTemplate": { + "item": { + "type": "Video", + "id": "VIDEO" + } + } +})apl"; + +TEST_F(LayoutTest, TopLevelDisallowedEditTextFailsInflation) { + config->set(RootProperty::kDisallowEditText, true); + + loadDocumentExpectFailure(SIMPLE_EDIT_TEXT_TOP); + + ASSERT_FALSE(component); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(LayoutTest, TopLevelDisallowedVideoFailsInflation) { + config->set(RootProperty::kDisallowVideo, true); + + loadDocumentExpectFailure(SIMPLE_VIDEO_TOP); + + ASSERT_FALSE(component); + ASSERT_TRUE(ConsoleMessage()); } \ No newline at end of file diff --git a/aplcore/unit/extension/unittest_extension_client.cpp b/aplcore/unit/extension/unittest_extension_client.cpp index b5f4180..b115750 100644 --- a/aplcore/unit/extension/unittest_extension_client.cpp +++ b/aplcore/unit/extension/unittest_extension_client.cpp @@ -36,7 +36,7 @@ class ExtensionClientTest : public DocumentWrapper { ASSERT_TRUE(content->isReady()); } - std::shared_ptr createClient(const std::string& extension) { + ExtensionClientPtr createClient(const std::string& extension) { return ExtensionClient::create(configPtr, extension, session); } @@ -49,8 +49,8 @@ class ExtensionClientTest : public DocumentWrapper { } } - std::shared_ptr configPtr; - std::shared_ptr client; + RootConfigPtr configPtr; + ExtensionClientPtr client; rapidjson::Document doc; void SetUp() override @@ -3466,4 +3466,103 @@ TEST_F(ExtensionClientTest, WrongLiveArray) { ASSERT_EQ("", text->getCalculated(kPropertyText).asString()); } +static const char* COMMANDS_EXT_DOC = R"({ +"type": "APL", +"version": "1.8", +"extension": { + "uri": "aplext:hello:10", + "name": "Hello" +}, +"mainTemplate": { + "item": { + "type": "Container", + "width": 500, + "height": 500, + "items": [ + { + "type": "TouchWrapper", + "width": "100%", + "height": "50%", + "onPress": [ + { + "type": "Hello:NotifyClasses", + "classes": ["English 101", "History", "Gymnastics"] + } + ] + }, + { + "type": "TouchWrapper", + "width": "100%", + "height": "50%", + "onPress": [ + { + "type": "Hello:NotifyClasses" + } + ] + } + ] + } + } +})"; + +TEST_F(ExtensionClientTest, DefaultPropertyValues) { + createConfigAndClient(COMMANDS_EXT_DOC); + + // Check what document wants. + auto extRequests = content->getExtensionRequests(); + ASSERT_EQ(1, extRequests.size()); + auto extRequest = *extRequests.begin(); + ASSERT_EQ("aplext:hello:10", extRequest); + + // Pass request and settings to connection request creation. + auto connectionRequest = client->createRegistrationRequest(doc.GetAllocator(), *content); + ASSERT_STREQ("aplext:hello:10", connectionRequest["uri"].GetString()); + + auto registerSuccessWithDefault = R"( + { + "method": "RegisterSuccess", + "version": "1.0", + "token": "TOKEN", + "schema": { + "type": "Schema", + "version": "1.0", + "uri": "aplext:hello:10", + "types": [ + { + "name": "User", + "properties": { + "classes": { + "type": "object", + "required": false, + "default": ["Math", "CS", "Physics"] + } + } + } + ], + "commands": [ + { + "name": "NotifyClasses", + "payload": "User" + } + ] + } + })"; + + // Runtime asked for connection. Process Schema message + ASSERT_TRUE(client->processMessage(nullptr, registerSuccessWithDefault)); + initializeContext(); + + // Trigger command with document supplied property + performTap(100, 100); + auto event = root->popEvent(); + auto payload = event.getValue(apl::kEventPropertyExtension).getMap(); + ASSERT_EQ(apl::ObjectArray({"English 101", "History", "Gymnastics"}), payload["classes"].getArray()); + + // Trigger command with default property + performTap(300, 300); + event = root->popEvent(); + payload = event.getValue(apl::kEventPropertyExtension).getMap(); + ASSERT_EQ(apl::ObjectArray({"Math", "CS", "Physics"}), payload["classes"].getArray()); +} + #endif diff --git a/aplcore/unit/extension/unittest_extension_mediator.cpp b/aplcore/unit/extension/unittest_extension_mediator.cpp index 6226344..8dc745c 100644 --- a/aplcore/unit/extension/unittest_extension_mediator.cpp +++ b/aplcore/unit/extension/unittest_extension_mediator.cpp @@ -602,7 +602,7 @@ class ExtensionMediatorTest : public DocumentWrapper { alexaext::Executor::getSynchronousExecutor()); } - void loadExtensions(const char* document) { + void loadExtensions(const char* document, const ObjectMap& flags = ObjectMap{}) { createContent(document, nullptr); if (!extensionProvider) { @@ -617,7 +617,7 @@ class ExtensionMediatorTest : public DocumentWrapper { ensureRequestedExtensions(content->getExtensionRequests()); // load them into config via the mediator - mediator->loadExtensions(config, content); + mediator->loadExtensions(flags, content); } void ensureRequestedExtensions(std::set requestedExtensions) { @@ -849,8 +849,7 @@ TEST_F(ExtensionMediatorTest, RegistrationConfig) { * Test that runtime flags are passed to the extension. */ TEST_F(ExtensionMediatorTest, RegistrationFlags) { - config->registerExtensionFlags("aplext:hello:10", "--hello"); - loadExtensions(EXT_DOC); + loadExtensions(EXT_DOC, ObjectMap{{"aplext:hello:10", "--hello"}}); // direct access to extension for test inspection auto hello = testExtensions["aplext:hello:10"].lock(); @@ -1692,9 +1691,9 @@ TEST_F(ExtensionMediatorTest, TestRegistrationSchema) { extensionProvider->registerExtension(adapter); createContent(SIMPLE_EXT_DOC, nullptr); - mediator->initializeExtensions(config, content); - config->registerExtensionFlags(TEST_EXTENSION_URI, "--testflag"); - mediator->loadExtensions(config, content, [](){}); + auto flagsMap = ObjectMap{{TEST_EXTENSION_URI, "--testflag"}}; + mediator->initializeExtensions(flagsMap, content); + mediator->loadExtensions(flagsMap, content, [](){}); ASSERT_TRUE(adapter->hasPendingRequest(TEST_EXTENSION_URI)); auto registerRequest = adapter->getPendingRequest(TEST_EXTENSION_URI); @@ -1733,12 +1732,12 @@ TEST_F(ExtensionMediatorTest, FastInitialization) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); ASSERT_TRUE(adapter->isInitialized(TEST_EXTENSION_URI)); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); @@ -1752,6 +1751,8 @@ TEST_F(ExtensionMediatorTest, FastInitialization) { ASSERT_TRUE(adapter->isRegistered(TEST_EXTENSION_URI)); ASSERT_TRUE(*loaded); + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); + ASSERT_EQ(TEST_EXTENSION_URI, mediator->getLoadedExtensions().find(TEST_EXTENSION_URI)->second->getURI()); // Finalize now mediator->finish(); @@ -1772,18 +1773,19 @@ TEST_F(ExtensionMediatorTest, FastInitializationFailInitialize) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); ASSERT_FALSE(adapter->isInitialized(TEST_EXTENSION_URI)); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); ASSERT_FALSE(adapter->isRegistered(TEST_EXTENSION_URI)); // Still considered loaded. Extension just not available. ASSERT_TRUE(*loaded); + ASSERT_EQ(0, mediator->getLoadedExtensions().size()); ASSERT_TRUE(ConsoleMessage()); } @@ -1801,17 +1803,18 @@ TEST_F(ExtensionMediatorTest, FastInitializationFailRegistrationRequest) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); ASSERT_TRUE(adapter->isInitialized(TEST_EXTENSION_URI)); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); ASSERT_FALSE(adapter->isRegistered(TEST_EXTENSION_URI)); ASSERT_TRUE(*loaded); + ASSERT_EQ(0, mediator->getLoadedExtensions().size()); ASSERT_TRUE(ConsoleMessage()); } @@ -1829,12 +1832,12 @@ TEST_F(ExtensionMediatorTest, FastInitializationFailRegistration) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); ASSERT_TRUE(adapter->isInitialized(TEST_EXTENSION_URI)); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); @@ -1849,6 +1852,7 @@ TEST_F(ExtensionMediatorTest, FastInitializationFailRegistration) { ASSERT_FALSE(adapter->isRegistered(TEST_EXTENSION_URI)); ASSERT_TRUE(*loaded); + ASSERT_EQ(0, mediator->getLoadedExtensions().size()); } @@ -1868,7 +1872,7 @@ TEST_F(ExtensionMediatorTest, FastInitializationGranted) { ASSERT_TRUE(content->isReady()); // grant extension access - mediator->initializeExtensions(config, content, + mediator->initializeExtensions(ObjectMap{}, content, [](const std::string& uri, ExtensionMediator::ExtensionGrantResult grant, ExtensionMediator::ExtensionGrantResult deny) { grant(uri); @@ -1877,7 +1881,7 @@ TEST_F(ExtensionMediatorTest, FastInitializationGranted) { ASSERT_TRUE(adapter->isInitialized(TEST_EXTENSION_URI)); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); @@ -1891,7 +1895,8 @@ TEST_F(ExtensionMediatorTest, FastInitializationGranted) { ASSERT_TRUE(adapter->isRegistered(TEST_EXTENSION_URI)); ASSERT_TRUE(*loaded); - + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); + ASSERT_EQ(TEST_EXTENSION_URI, mediator->getLoadedExtensions().find(TEST_EXTENSION_URI)->second->getURI()); } @@ -1911,13 +1916,14 @@ TEST_F(ExtensionMediatorTest, FastInitializationDenied) { ASSERT_TRUE(content->isReady()); // deny extension access - mediator->initializeExtensions(config, content, + mediator->initializeExtensions(ObjectMap{}, content, [](const std::string& uri, ExtensionMediator::ExtensionGrantResult grant, ExtensionMediator::ExtensionGrantResult deny) { deny(uri); }); ASSERT_FALSE(adapter->isInitialized(TEST_EXTENSION_URI)); + ASSERT_EQ(0, mediator->getLoadedExtensions().size()); } @@ -1938,7 +1944,7 @@ TEST_F(ExtensionMediatorTest, FastInitializationMissingGrant) { // grant extension access auto grantRequest = std::make_shared(false); - mediator->initializeExtensions(config, content, + mediator->initializeExtensions(ObjectMap{}, content, [grantRequest](const std::string& uri, ExtensionMediator::ExtensionGrantResult grant, ExtensionMediator::ExtensionGrantResult deny) { //neither grant nor deny @@ -1948,13 +1954,14 @@ TEST_F(ExtensionMediatorTest, FastInitializationMissingGrant) { ASSERT_FALSE(adapter->isInitialized(TEST_EXTENSION_URI)); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); ASSERT_TRUE(LogMessage()); ASSERT_TRUE(*loaded); ASSERT_FALSE(adapter->isRegistered(TEST_EXTENSION_URI)); + ASSERT_EQ(0, mediator->getLoadedExtensions().size()); } TEST_F(ExtensionMediatorTest, RootConfigNull) { @@ -1974,7 +1981,7 @@ TEST_F(ExtensionMediatorTest, RootConfigNull) { // grant extension access auto grantRequest = std::make_shared(false); - mediator->initializeExtensions(config, content, + mediator->initializeExtensions(ObjectMap{}, content, [grantRequest](const std::string& uri, ExtensionMediator::ExtensionGrantResult grant, ExtensionMediator::ExtensionGrantResult deny) { //neither grant nor deny @@ -1984,7 +1991,7 @@ TEST_F(ExtensionMediatorTest, RootConfigNull) { ASSERT_FALSE(adapter->isInitialized(TEST_EXTENSION_URI)); auto loaded = std::make_shared(false); - mediator->loadExtensions(nullptr, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); ASSERT_TRUE(LogMessage()); @@ -2010,7 +2017,7 @@ TEST_F(ExtensionMediatorTest, LoadGranted) { // explicit grant of test extensions auto granted = adapter->getURIs(); - mediator->loadExtensions(config, content, &granted); + mediator->loadExtensions(ObjectMap{}, content, &granted); ASSERT_TRUE(adapter->isInitialized(TEST_EXTENSION_URI)); @@ -2040,7 +2047,7 @@ TEST_F(ExtensionMediatorTest, LoadDenied) { // empty set results in all extension denied std::set granted; - mediator->loadExtensions(config, content, &granted); + mediator->loadExtensions(ObjectMap{}, content, &granted); ASSERT_FALSE(adapter->isInitialized(TEST_EXTENSION_URI)); } @@ -2061,7 +2068,7 @@ TEST_F(ExtensionMediatorTest, LoadAllGranted) { ASSERT_TRUE(content->isReady()); // when content ready, unspecified grant list means all extensions granted - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); ASSERT_TRUE(adapter->isInitialized(TEST_EXTENSION_URI)); @@ -2105,7 +2112,7 @@ TEST_F(ExtensionMediatorTest, LoadContentNotReady) { // when content ready, unspecified grant list means all extensions granted // without ready content load not attempted - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); ASSERT_TRUE(ConsoleMessage()); ASSERT_FALSE(adapter->isInitialized(TEST_EXTENSION_URI)); @@ -2214,10 +2221,10 @@ TEST_F(ExtensionMediatorTest, ComponentInteractions) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); @@ -2331,11 +2338,11 @@ TEST_F(ExtensionMediatorTest, ComponentCommands) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); auto loaded = std::make_shared(false); auto callCount = std::make_shared(0); - mediator->loadExtensions(config, content, [loaded, callCount](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded, callCount](){ *loaded = true; (*callCount)++; }); @@ -2460,10 +2467,10 @@ TEST_F(ExtensionMediatorTest, ComponentEventCorrect) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); @@ -2514,10 +2521,10 @@ TEST_F(ExtensionMediatorTest, ComponentEventWithoutResource) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); @@ -2562,10 +2569,10 @@ TEST_F(ExtensionMediatorTest, DocumentEventCorrect) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); @@ -2614,10 +2621,10 @@ TEST_F(ExtensionMediatorTest, DocumentEventWithResourceId) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); @@ -2671,10 +2678,10 @@ TEST_F(ExtensionMediatorTest, DocumentEventBeforeRegistrationFinished) { .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); auto loaded = std::make_shared(false); - mediator->loadExtensions(config, content, [loaded](){ + mediator->loadExtensions(ObjectMap{}, content, [loaded](){ *loaded = true; }); @@ -2707,7 +2714,7 @@ TEST_F(ExtensionMediatorTest, ExtensionComponentWithoutProxy) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); // Provide component definition without registering extension ExtensionComponentDefinition componentDef = ExtensionComponentDefinition("alexaext:example:10", "Example"); @@ -2740,7 +2747,7 @@ TEST_F(ExtensionMediatorTest, ExtensionComponentNotifyFailed) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); inflate(); ASSERT_TRUE(ConsoleMessage()); @@ -2761,7 +2768,7 @@ TEST_F(ExtensionMediatorTest, ExtensionComponentResourceProviderError) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); inflate(); ASSERT_TRUE(root); @@ -2808,11 +2815,11 @@ TEST_F(ExtensionMediatorTest, ExtensionProviderFaultTest) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); // To mock a faulty provider that returns null proxy for an initialized extension std::static_pointer_cast(extensionProvider)->returnNullProxy(true); - mediator->loadExtensions(config, content, [](){}); + mediator->loadExtensions(ObjectMap{}, content, [](){}); inflate(); ASSERT_TRUE(ConsoleMessage()); @@ -2871,8 +2878,10 @@ TEST_F(ExtensionMediatorTest, BasicExtensionLifecycle) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); + auto activity = mediator->getLoadedExtensions().at(LifecycleTestExtension::URI); ASSERT_NE("", extension->lastActivity.getId()); @@ -2901,6 +2910,7 @@ TEST_F(ExtensionMediatorTest, BasicExtensionLifecycle) { ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateHidden})); ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_EQ(*activity, extension->lastActivity); ASSERT_TRUE(CheckSendEvent(root, "ExtensionReadyReceived")); } @@ -2926,11 +2936,12 @@ TEST_F(ExtensionMediatorTest, SessionUsedAcrossDocuments) { config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) .extensionProvider(extensionProvider) .extensionMediator(mediator); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); ASSERT_NE("", extension->lastActivity.getId()); auto firstDocumentActivity = extension->lastActivity; + ASSERT_EQ(firstDocumentActivity, *mediator->getLoadedExtensions().at(LifecycleTestExtension::URI)); inflate(); ASSERT_TRUE(root); @@ -2957,10 +2968,11 @@ TEST_F(ExtensionMediatorTest, SessionUsedAcrossDocuments) { config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) .extensionProvider(extensionProvider) .extensionMediator(mediator); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); ASSERT_NE(firstDocumentActivity, extension->lastActivity); + ASSERT_EQ(extension->lastActivity, *mediator->getLoadedExtensions().at(LifecycleTestExtension::URI)); inflate(); ASSERT_TRUE(root); @@ -2999,8 +3011,8 @@ TEST_F(ExtensionMediatorTest, SessionEndedBeforeDocumentFinished) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); ASSERT_NE("", extension->lastActivity.getId()); @@ -3039,8 +3051,8 @@ TEST_F(ExtensionMediatorTest, SessionEndedBeforeDocumentRendered) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); inflate(); @@ -3072,8 +3084,8 @@ TEST_F(ExtensionMediatorTest, SessionEndedBeforeExtensionsLoaded) { ASSERT_TRUE(content->isReady()); session->end(); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); inflate(); @@ -3151,10 +3163,14 @@ TEST_F(ExtensionMediatorTest, SessionEndsAfterAllActivitiesHaveFinished) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); ASSERT_NE("", extension->lastActivity.getId()); + auto activity1 = mediator->getLoadedExtensions().at("test:lifecycle:1.0"); + auto activity2 = mediator->getLoadedExtensions().at("test:lifecycleOther:2.0"); + ASSERT_EQ(extension->lastActivity, *activity1); + ASSERT_EQ(otherExtension->lastActivity, *activity2); inflate(); @@ -3221,7 +3237,9 @@ TEST_F(ExtensionMediatorTest, RejectedExtensionsDoNotPreventEndingSessions) { std::set grantedExtensions = {"test:lifecycle:1.0"}; - mediator->loadExtensions(config, content, &grantedExtensions); + mediator->loadExtensions(ObjectMap{}, content, &grantedExtensions); + auto activity = mediator->getLoadedExtensions().at("test:lifecycle:1.0"); + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); ASSERT_NE("", extension->lastActivity.getId()); @@ -3238,6 +3256,7 @@ TEST_F(ExtensionMediatorTest, RejectedExtensionsDoNotPreventEndingSessions) { ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); ASSERT_TRUE(extension->verifyNoMoreInteractions()); + ASSERT_EQ(*activity, extension->lastActivity); // Check that there were no interactions with the denied extension ASSERT_TRUE(otherExtension->verifyNoMoreInteractions()); @@ -3268,8 +3287,9 @@ TEST_F(ExtensionMediatorTest, FailureDuringRegistrationDoesNotPreventEndingSessi std::set grantedExtensions = {"test:lifecycle:1.0"}; - mediator->loadExtensions(config, content); - + mediator->loadExtensions(ObjectMap{}, content); + auto activity = mediator->getLoadedExtensions().at("test:lifecycle:1.0"); + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); ASSERT_NE("", extension->lastActivity.getId()); inflate(); @@ -3285,6 +3305,7 @@ TEST_F(ExtensionMediatorTest, FailureDuringRegistrationDoesNotPreventEndingSessi ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); ASSERT_TRUE(extension->verifyNoMoreInteractions()); + ASSERT_EQ(*activity, extension->lastActivity); // Check that there were no interactions with the denied extension ASSERT_TRUE(otherExtension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); @@ -3316,7 +3337,9 @@ TEST_F(ExtensionMediatorTest, RejectedRegistrationDoesNotPreventEndingSessions) std::set grantedExtensions = {"test:lifecycle:1.0"}; - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); + auto activity = mediator->getLoadedExtensions().at("test:lifecycle:1.0"); + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); ASSERT_NE("", extension->lastActivity.getId()); @@ -3333,6 +3356,7 @@ TEST_F(ExtensionMediatorTest, RejectedRegistrationDoesNotPreventEndingSessions) ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); ASSERT_TRUE(extension->verifyNoMoreInteractions()); + ASSERT_EQ(*activity, extension->lastActivity); ASSERT_TRUE(ConsoleMessage()); // Consume the failed registration console message } @@ -3363,7 +3387,9 @@ TEST_F(ExtensionMediatorTest, MissingProxyDoesNotPreventEndingSessions) { std::set grantedExtensions = {"test:lifecycle:1.0"}; - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); + auto activity = mediator->getLoadedExtensions().at("test:lifecycle:1.0"); + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); ASSERT_NE("", extension->lastActivity.getId()); @@ -3380,6 +3406,7 @@ TEST_F(ExtensionMediatorTest, MissingProxyDoesNotPreventEndingSessions) { ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); ASSERT_TRUE(extension->verifyNoMoreInteractions()); + ASSERT_EQ(*activity, extension->lastActivity); ASSERT_TRUE(ConsoleMessage()); } @@ -3406,7 +3433,9 @@ TEST_F(ExtensionMediatorTest, UnknownExtensionDoesNotPreventEndingSessions) { std::set grantedExtensions = {"test:lifecycle:1.0"}; - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); + auto activity = mediator->getLoadedExtensions().at("test:lifecycle:1.0"); + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); ASSERT_NE("", extension->lastActivity.getId()); @@ -3423,6 +3452,7 @@ TEST_F(ExtensionMediatorTest, UnknownExtensionDoesNotPreventEndingSessions) { ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); ASSERT_TRUE(extension->verifyNoMoreInteractions()); + ASSERT_EQ(*activity, extension->lastActivity); } TEST_F(ExtensionMediatorTest, BrokenProviderDoesNotPreventEndingSessions) { @@ -3458,7 +3488,9 @@ TEST_F(ExtensionMediatorTest, BrokenProviderDoesNotPreventEndingSessions) { std::set grantedExtensions = {"test:lifecycle:1.0"}; - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); + auto activity = mediator->getLoadedExtensions().at("test:lifecycle:1.0"); + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); ASSERT_NE("", extension->lastActivity.getId()); @@ -3475,6 +3507,7 @@ TEST_F(ExtensionMediatorTest, BrokenProviderDoesNotPreventEndingSessions) { ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); ASSERT_TRUE(extension->verifyNoMoreInteractions()); + ASSERT_EQ(*activity, extension->lastActivity); ASSERT_TRUE(ConsoleMessage()); } @@ -3503,7 +3536,9 @@ TEST_F(ExtensionMediatorTest, FailureToInitializeDoesNotPreventEndingSessions) { std::set grantedExtensions = {"test:lifecycle:1.0"}; - mediator->loadExtensions(config, content); + mediator->loadExtensions(ObjectMap{}, content); + auto activity = mediator->getLoadedExtensions().at("test:lifecycle:1.0"); + ASSERT_EQ(1, mediator->getLoadedExtensions().size()); ASSERT_NE("", extension->lastActivity.getId()); @@ -3520,6 +3555,7 @@ TEST_F(ExtensionMediatorTest, FailureToInitializeDoesNotPreventEndingSessions) { ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); ASSERT_TRUE(extension->verifyNoMoreInteractions()); + ASSERT_EQ(*activity, extension->lastActivity); ASSERT_TRUE(ConsoleMessage()); // Consume the failed initialization console message } @@ -3570,8 +3606,8 @@ TEST_F(ExtensionMediatorTest, LifecycleWithComponent) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); ASSERT_NE("", extension->lastActivity.getId()); @@ -3668,8 +3704,8 @@ TEST_F(ExtensionMediatorTest, LifecycleWithLiveData) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); ASSERT_NE("", extension->lastActivity.getId()); @@ -3726,8 +3762,8 @@ TEST_F(ExtensionMediatorTest, LifecycleAPIsRespectExtensionToken) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); inflate(); ASSERT_TRUE(root); @@ -3917,8 +3953,8 @@ TEST_F(ExtensionMediatorTest, ExtensionComponentSchema) { .extensionProvider(extensionProvider) .extensionMediator(mediator); ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); - mediator->loadExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); + mediator->loadExtensions(ObjectMap{}, content); inflate(); @@ -4058,9 +4094,9 @@ TEST_F(ExtensionMediatorTest, RequiredExtension) { ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); bool loaded = false; - mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); inflate(); @@ -4124,9 +4160,9 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionRegistrationFail) { ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); bool loaded = false; - mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); inflate(); @@ -4134,6 +4170,132 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionRegistrationFail) { ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); } +TEST_F(ExtensionMediatorTest, RequiredExtensionUnregistered) { + auto extSession = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + createContent(REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(ObjectMap{}, content); + bool loaded = false; + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_FALSE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); + session->checkAndClear("Provider doesn't have required extension: test:required:1.0"); +} + +const char* EXPLICIT_UNREQUIRED_EXTENSION = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "extensions": [ + { + "uri": "test:unrequired:1.0", + "name": "Unrequired", + "required": false + } + ], + "mainTemplate": { + "item": { + "type": "Text", + "width": "100%", + "height": "100%", + "text": "${environment.extension.Unrequired}" + } + } +})"; + +TEST_F(ExtensionMediatorTest, ExplicitUnrequiredExtensionUnregistered) { + auto extSession = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + createContent(EXPLICIT_UNREQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(ObjectMap{}, content); + bool loaded = false; + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_TRUE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); +} + +const char* IMPLICIT_UNREQUIRED_EXTENSION = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "extensions": [ + { + "uri": "test:unrequired:1.0", + "name": "Unrequired" + } + ], + "mainTemplate": { + "item": { + "type": "Text", + "width": "100%", + "height": "100%", + "text": "${environment.extension.Unrequired}" + } + } +})"; + +TEST_F(ExtensionMediatorTest, ImplicitUnrequiredExtensionUnregistered) { + auto extSession = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + createContent(IMPLICIT_UNREQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(ObjectMap{}, content); + bool loaded = false; + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_TRUE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); +} + TEST_F(ExtensionMediatorTest, RequiredExtensionDenied) { auto session = ExtensionSession::create(); @@ -4155,13 +4317,13 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionDenied) { ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content, [](const std::string& uri, + mediator->initializeExtensions(ObjectMap{}, content, [](const std::string& uri, ExtensionMediator::ExtensionGrantResult grant, ExtensionMediator::ExtensionGrantResult deny) { deny(REQUIRED_URI); }); bool loaded = false; - mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); inflate(); @@ -4246,9 +4408,9 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionRemote) { ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); bool loaded = false; - mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); proxy->processRegistration(); inflate(); @@ -4304,9 +4466,9 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteDouble) { ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); bool loaded = false; - mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); inflate(); @@ -4363,9 +4525,9 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteDoubleNamed) { ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); bool loaded = false; - mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); inflate(); @@ -4396,9 +4558,9 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteInitFail) { ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); bool loaded = false; - mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); inflate(); @@ -4429,9 +4591,9 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteRequestFail) { ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); bool loaded = false; - mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); inflate(); @@ -4462,9 +4624,9 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteRegistrationFail) { ASSERT_TRUE(content->isReady()); - mediator->initializeExtensions(config, content); + mediator->initializeExtensions(ObjectMap{}, content); bool loaded = false; - mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + mediator->loadExtensions(ObjectMap{}, content, [&](bool result){ loaded = result; }); proxy->processRegistration(); inflate(); diff --git a/aplcore/unit/extension/unittest_requested_extension.cpp b/aplcore/unit/extension/unittest_requested_extension.cpp index 2b25fbf..d0cb54d 100644 --- a/aplcore/unit/extension/unittest_requested_extension.cpp +++ b/aplcore/unit/extension/unittest_requested_extension.cpp @@ -790,3 +790,46 @@ TEST_F(RequestedExtensionTest, SettingsWithMultiPackage) { ASSERT_TRUE(IsEqual("package1-D", es.get("keyD"))); ASSERT_TRUE(IsEqual("package2-E", es.get("keyE"))); } + +static const char* EVALUATED_SETTINGS = R"({ + "type": "APL", + "version": "1.2", + "extension": { + "uri": "URI1", + "name": "foo" + }, + "settings": { + "foo": { + "keyA": "${environment.customSetting}", + "keyB": "valueB" + } + }, + "mainTemplate": { + "item": { + "type": "Text" + } + } +})"; + + +/** + * Document provides extension settings + */ +TEST_F(RequestedExtensionTest, EvaluatedSettings) { + config->setEnvironmentValue("customSetting", "valueA"); + + createContent(EVALUATED_SETTINGS, nullptr, true); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // verify extensions available + ASSERT_TRUE(IsEqual(Object::TRUE_OBJECT(), evaluate(*context, "${environment.extension.foo}"))); + + // verify settings on the named extension + auto es = content->getExtensionSettings("URI1"); + ASSERT_FALSE(IsEqual(Object::NULL_OBJECT(), es)); + + ASSERT_TRUE(IsEqual("valueA", es.get("keyA"))); + ASSERT_TRUE(IsEqual("valueB", es.get("keyB"))); +} \ No newline at end of file diff --git a/aplcore/unit/focus/unittest_focus_manager.cpp b/aplcore/unit/focus/unittest_focus_manager.cpp index 8385d71..a41255d 100644 --- a/aplcore/unit/focus/unittest_focus_manager.cpp +++ b/aplcore/unit/focus/unittest_focus_manager.cpp @@ -103,6 +103,43 @@ TEST_F(FocusManagerTest, ManualControl) ASSERT_FALSE(root->hasEvent()); } +static const char *FOCUS_SCROLL_TEST = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Sequence", + "id": "foo", + "width": 200, + "height": 300, + "items": { + "type": "TouchWrapper", + "id": "item${index}", + "width": 100, + "height": 100 + }, + "data": "${Array.range(1,11)}" + } + } +})"; + +TEST_F(FocusManagerTest, SetFocusTermination) +{ + loadDocument(FOCUS_SCROLL_TEST); + auto thing1 = CoreComponent::cast(root->context().findComponentById("item5")); + ASSERT_TRUE(thing1); + + auto& fm = root->context().focusManager(); + ASSERT_FALSE(fm.getFocus()); + + // This will cause a scroll action to be taken. + fm.setFocus(thing1, true); + + // We need to tear down the root context while the scroll action is active + // so that callback runs during the destructor. + context = nullptr; +} + TEST_F(FocusManagerTest, ManualControlDontNotifyViewhost) { loadDocument(FOCUS_TEST); diff --git a/aplcore/unit/graphic/unittest_graphic.cpp b/aplcore/unit/graphic/unittest_graphic.cpp index 31772c1..f8471e7 100644 --- a/aplcore/unit/graphic/unittest_graphic.cpp +++ b/aplcore/unit/graphic/unittest_graphic.cpp @@ -3454,8 +3454,9 @@ static const char *GRAPHIC_ELEMENT_MISSING_WIDTH = R"apl( // Verify that filters serialize correctly TEST_F(GraphicTest, MissingWidthDoesntStop) { - auto content = apl::Content::create(GRAPHIC_ELEMENT_MISSING_WIDTH); + auto content = apl::Content::create(GRAPHIC_ELEMENT_MISSING_WIDTH, session); ASSERT_TRUE(content->isReady()); ASSERT_TRUE(apl::RootContext::create( apl::Metrics().size(1280, 800).dpi(160).shape(apl::ScreenShape::ROUND), content)); + ASSERT_TRUE(session->checkAndClear()); } diff --git a/aplcore/unit/graphic/unittest_graphic_component.cpp b/aplcore/unit/graphic/unittest_graphic_component.cpp index a33354d..d0388a9 100644 --- a/aplcore/unit/graphic/unittest_graphic_component.cpp +++ b/aplcore/unit/graphic/unittest_graphic_component.cpp @@ -2866,4 +2866,95 @@ TEST_F(GraphicComponentTest, Characteristics) ASSERT_TRUE(vg->isFocusable()); ASSERT_TRUE(vg->isTouchable()); ASSERT_TRUE(vg->isActionable()); -} \ No newline at end of file +} + +static const char* PARAMETER_PASSING_VARIANTS = R"({ + "type": "APL", + "version": "2023.3", + "graphics": { + "SpeechBubble": { + "type": "AVG", + "version": "1.0", + "height": 100, + "width": 100, + "lang": "en-US", + "parameters": [ + "MyColor", + "speech" + ], + "items": [ + { + "type": "text", + "fill": "${MyColor}", + "fontSize": 20, + "text": "${speech}", + "x": 50, + "y": 50, + "textAnchor": "middle" + } + ] + } + }, + "mainTemplate": { + "item": { + "type": "Container", + "items": [ + { + "type": "VectorGraphic", + "id": "ImplicitParameters", + "source": "SpeechBubble", + "MyColor": "red", + "speech": "A" + }, + { + "type": "VectorGraphic", + "id": "ExplicitParameters", + "source": "SpeechBubble", + "parameters": { + "MyColor": "blue", + "speech": "B" + } + }, + { + "type": "VectorGraphic", + "id": "MixedParameters", + "source": "SpeechBubble", + "parameters": { + "MyColor": "blue" + }, + "MyColor": "red", + "speech": "C" + } + ] + } + } +})"; + +TEST_F(GraphicComponentTest, ParameterPassingVariants) { + loadDocument(PARAMETER_PASSING_VARIANTS); + + { + auto component = context->findComponentById("ImplicitParameters"); + auto graphic = component->getCalculated(kPropertyGraphic).get(); + auto text = graphic->getRoot()->getChildAt(0); + ASSERT_EQ(kGraphicElementTypeText, text->getType()); + ASSERT_EQ(Object(Color(Color::RED)), text->getValue(kGraphicPropertyFill)); + ASSERT_EQ("A", text->getValue(kGraphicPropertyText).asString()); + } + + { + auto component = context->findComponentById("ExplicitParameters"); + auto graphic = component->getCalculated(kPropertyGraphic).get(); + auto text = graphic->getRoot()->getChildAt(0); + ASSERT_EQ(Object(Color(Color::BLUE)), text->getValue(kGraphicPropertyFill)); + ASSERT_EQ("B", text->getValue(kGraphicPropertyText).asString()); + } + + { + auto component = context->findComponentById("MixedParameters"); + auto graphic = component->getCalculated(kPropertyGraphic).get(); + auto text = graphic->getRoot()->getChildAt(0); + ASSERT_EQ(Object(Color(Color::BLUE)), text->getValue(kGraphicPropertyFill)); + ASSERT_EQ("", text->getValue(kGraphicPropertyText).asString()); + } +} diff --git a/aplcore/unit/livedata/unittest_livearray_rebuild.cpp b/aplcore/unit/livedata/unittest_livearray_rebuild.cpp index fdd2041..d978b26 100644 --- a/aplcore/unit/livedata/unittest_livearray_rebuild.cpp +++ b/aplcore/unit/livedata/unittest_livearray_rebuild.cpp @@ -1598,7 +1598,7 @@ TEST_F(LiveArrayRebuildTest, DeepComponentUpdate) ASSERT_TRUE(CheckDirty(component)); ASSERT_TRUE(CheckDirty(component->getChildAt(0)->getChildAt(0), kPropertyText, kPropertyVisualHash)); - ASSERT_TRUE(CheckDirty(component->getChildAt(0), kPropertyBackgroundColor, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(component->getChildAt(0), kPropertyBackgroundColor, kPropertyBackground, kPropertyVisualHash)); ASSERT_EQ("update", component->getChildAt(0)->getChildAt(0)->getCalculated(kPropertyText).asString()); ASSERT_EQ(Color(0x0000FFFF), component->getChildAt(0)->getCalculated(kPropertyBackgroundColor).getColor()); diff --git a/aplcore/unit/media/unittest_media_player.cpp b/aplcore/unit/media/unittest_media_player.cpp index db1d0ff..b1c0e6f 100644 --- a/aplcore/unit/media/unittest_media_player.cpp +++ b/aplcore/unit/media/unittest_media_player.cpp @@ -18,7 +18,6 @@ #include #include "testmediaplayerfactory.h" -#include "apl/component/videocomponent.h" using namespace apl; @@ -1633,3 +1632,41 @@ TEST_F(MediaPlayerTest, MuteVideo) { ASSERT_TRUE(testMediaPlayer->isMuted()); } + +static const char* VIDEO_IN_CONTAINER_WITH_AUTOPLAY = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "items": { + "type": "Container", + "width": 200, + "height": 200, + "items": { + "type": "Video", + "id": "VIDEO", + "autoplay": true, + "width": "100%", + "height": "100%", + "onPlay": { + "type": "SendEvent", + "sequencer": "FOO", + "arguments": [ + "Handler: ${event.source.handler}" + ] + } + } + } + } +})"; + +TEST_F(MediaPlayerTest, AutoplayDoesntPlayVideoWhenDisallowVideoTrue) { + config->set(RootProperty::kDisallowVideo, true); + loadDocument(VIDEO_IN_CONTAINER_WITH_AUTOPLAY); + + ASSERT_TRUE(component); + auto v = std::static_pointer_cast(root->findComponentById("VIDEO")); + // No media player when disallow is true + ASSERT_EQ(v->getMediaPlayer(), nullptr); + // onPlay was not triggered + ASSERT_FALSE(root->hasEvent()); +} diff --git a/aplcore/unit/primitives/unittest_unicode.cpp b/aplcore/unit/primitives/unittest_unicode.cpp index bd135ff..11cac19 100644 --- a/aplcore/unit/primitives/unittest_unicode.cpp +++ b/aplcore/unit/primitives/unittest_unicode.cpp @@ -153,6 +153,34 @@ TEST_F(UnicodeTest, StringSlice) { ASSERT_EQ(m.expected, utf8StringSlice(m.original, m.start, m.end)); } +struct StringCharAtTest { + std::string original; + int index; + std::string expected; +}; + +static auto STRING_CHAR_AT_TESTS = std::vector { + { u8"", 0, u8"" }, + { u8"abcde", 0, u8"a" }, + { u8"abcde", 1, u8"b" }, + { u8"abcde", 3, u8"d" }, + { u8"abcde", 10, u8"" }, + { u8"abcde", -3, u8"c"}, // Negative offset + { u8"abcde", -100, u8""}, // Seriously negative offset + { u8"hémidécérébellé", 1, u8"é"}, + { u8"hémidécérébellé", 4, u8"d"}, + { u8"hémidécérébellé", 8, u8"r"}, + { u8"é", -1, u8"é"}, + { u8"عمر خیّام‎", 0, u8"ع" }, // RTL +}; + +TEST_F(UnicodeTest, StringCharAt) +{ + for (const auto& m : STRING_CHAR_AT_TESTS) + ASSERT_EQ(m.expected, utf8StringCharAt(m.original, m.index)); +} + + struct StripTest { std::string original; std::string valid; diff --git a/aplcore/unit/scaling/unittest_auto_size.cpp b/aplcore/unit/scaling/unittest_auto_size.cpp index 474aec9..d61305e 100644 --- a/aplcore/unit/scaling/unittest_auto_size.cpp +++ b/aplcore/unit/scaling/unittest_auto_size.cpp @@ -18,8 +18,44 @@ using namespace apl; -class AutoSizeTest : public apl::DocumentWrapper {}; +class AutoSizeTest : public apl::DocumentWrapper { +public: + ::testing::AssertionResult + doInitialize(const char *document, float width, float height) { + loadDocument(document); + if (!component) + return ::testing::AssertionFailure() << "Failed to load document"; + return IsEqual(Rect(0, 0, width, height), component->getCalculated(kPropertyBounds)); + } + + ::testing::AssertionResult + doTest(const char *property, int value, float width, float height) { + executeCommand("SetValue", {{"componentId", "FOO"}, {"property", property}, {"value", value}}, true); + root->clearPending(); + return checkComponent(width, height); + } + + ::testing::AssertionResult + doTest(const char *property, const char *value, float width, float height) { + executeCommand("SetValue", {{"componentId", "FOO"}, {"property", property}, {"value", value}}, true); + root->clearPending(); + return checkComponent(width, height); + } + + ::testing::AssertionResult + checkComponent(float width, float height) { + return CheckComponent(component, width, height); + } + ::testing::AssertionResult + checkViewport(float width, float height) { + return CheckViewport(root, width, height); + } +}; + +/** + * In this test the frame is small but set to auto-size. + */ static const char *BASIC_TEST = R"apl( { "type": "APL", @@ -27,8 +63,7 @@ static const char *BASIC_TEST = R"apl( "mainTemplate": { "item": { "type": "Frame", - "width": 123, - "height": 345 + "borderWidth": 100 } } } @@ -36,12 +71,292 @@ static const char *BASIC_TEST = R"apl( TEST_F(AutoSizeTest, Basic) { - metrics.size(300, 300).autoSizeHeight(true).autoSizeWidth(true); + metrics = Metrics().size(100, 100).minAndMaxHeight(50, 150).minAndMaxWidth(50, 150); loadDocument(BASIC_TEST); - ASSERT_TRUE(component); - ASSERT_TRUE(IsEqual(Rect(0,0,123,345), component->getCalculated(apl::kPropertyBounds))); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(150, 150)); + + // Fixed everything + metrics = Metrics().size(300, 300); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(300, 300)); + ASSERT_TRUE(checkViewport(300, 300)); + + // Fixed height, variable width + metrics = Metrics().size(300, 300).minAndMaxWidth(100, 500); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 300)); + ASSERT_TRUE(checkViewport(200, 300)); + + // Variable height, fixed width + metrics = Metrics().size(300, 300).minAndMaxHeight(100, 500); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(300, 200)); + ASSERT_TRUE(checkViewport(300, 200)); + + // Variable height and width + metrics = Metrics().size(300, 300).minAndMaxHeight(100, 500).minAndMaxWidth(50, 350); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(200, 200)); + + // These test cases use a viewport that starts at 150x150, which is smaller than the document + // Small: Fixed everything + metrics = Metrics().size(150, 150); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(150, 150)); + + // Small: Fixed height, variable width + metrics = Metrics().size(150, 150).minAndMaxWidth(100, 500); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(200, 150)); + + // Small: Variable height, fixed width + metrics = Metrics().size(150, 150).minAndMaxHeight(100, 500); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(150, 200)); + + // Small: Variable height and width + metrics = Metrics().size(150, 150).minAndMaxHeight(100, 500).minAndMaxWidth(50, 350); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(200, 200)); + + // Even smaller test cases where the variable size can't accommodate the entire Frame + // Tiny: Fixed everything + metrics = Metrics().size(100, 100); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(100, 100)); + + // Tiny: Fixed height, variable width + metrics = Metrics().size(100, 100).minAndMaxWidth(50, 150); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(150, 100)); + + // Tiny: Variable height, fixed width + metrics = Metrics().size(100, 100).minAndMaxHeight(50, 150); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(100, 150)); + + // Tiny: Variable height and width + metrics = Metrics().size(100, 100).minAndMaxHeight(50, 150).minAndMaxWidth(50, 150); + loadDocument(BASIC_TEST); + ASSERT_TRUE(checkComponent(200, 200)); + ASSERT_TRUE(checkViewport(150, 150)); +} + +/** + * Here the frame has a fixed size - it's not auto-sizing, so the viewport doesn't matter + */ +static const char *BASIC_BOUNDED_TEST = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Frame", + "width": 123, + "height": 345 + } + } +} +)apl"; + +TEST_F(AutoSizeTest, BasicBounded) +{ + // These test cases use a viewport that starts at 400x400 which is bigger than the document + // Fixed everything + metrics = Metrics().size(400, 400); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(400, 400)); + + // Fixed height, variable width + metrics = Metrics().size(400, 400).minAndMaxWidth(100, 500); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(123, 400)); + + // Variable height, fixed width + metrics = Metrics().size(400, 400).minAndMaxHeight(100, 500); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(400, 345)); + + // Variable height and width + metrics = Metrics().size(400, 400).minAndMaxHeight(100, 500).minAndMaxWidth(50, 350); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(123, 345)); + + // These test cases use a viewport that starts at 200x200, which sort-of in the document + // Small: Fixed everything + metrics = Metrics().size(200, 200); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(200, 200)); + + // Small: Fixed height, variable width + metrics = Metrics().size(200, 200).minAndMaxWidth(100, 500); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(123, 200)); + + // Small: Variable height, fixed width + metrics = Metrics().size(200, 200).minAndMaxHeight(100, 300); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(200, 300)); + + // Small: Variable height and width + metrics = Metrics().size(200, 200).minAndMaxHeight(100, 500).minAndMaxWidth(50, 350); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(123, 345)); + + // Even smaller test cases where the variable size can't accommodate the entire Frame + // Tiny: Fixed everything + metrics = Metrics().size(100, 100); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(100, 100)); + + // Tiny: Fixed height, variable width + metrics = Metrics().size(100, 100).minAndMaxWidth(50, 150); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(123, 100)); + + // Tiny: Variable height, fixed width + metrics = Metrics().size(100, 100).minAndMaxHeight(50, 150); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(100, 150)); + + // Tiny: Variable height and width + metrics = Metrics().size(100, 100).minAndMaxHeight(50, 150).minAndMaxWidth(50, 150); + loadDocument(BASIC_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(123, 345)); + ASSERT_TRUE(checkViewport(123, 150)); +} + +/** + * Here the frame has a fixed size in percentage + */ +static const char *PERCENTAGE_BOUNDED_TEST = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Frame", + "width": "50%", + "height": "30%" + } + } +} +)apl"; + +TEST_F(AutoSizeTest, PercentageBounded) +{ + // The frame is 50% the width of the viewport and 30% the height + // Fixed everything + metrics = Metrics().size(1000, 1000); + loadDocument(PERCENTAGE_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(500, 300)); + ASSERT_TRUE(checkViewport(1000, 1000)); + + // Fixed height, variable width + metrics = Metrics().size(1000, 1000).minAndMaxWidth(500, 1500); + loadDocument(PERCENTAGE_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(500, 300)); + ASSERT_TRUE(checkViewport(1000, 1000)); + + // Variable height, fixed width + metrics = Metrics().size(1000, 1000).minAndMaxHeight(500, 1500); + loadDocument(PERCENTAGE_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(500, 300)); + ASSERT_TRUE(checkViewport(1000, 1000)); + + // Variable height and width + metrics = Metrics().size(1000, 1000).minAndMaxHeight(500, 1500).minAndMaxWidth(500, 1500); + loadDocument(PERCENTAGE_BOUNDED_TEST); + ASSERT_TRUE(checkComponent(500, 300)); + ASSERT_TRUE(checkViewport(1000, 1000)); +} + +/** + * The wrapping test puts a bunch of 100x100 dp boxes in a container + * with wrapping set to true. The container auto-sizes in width and height + */ +static const char *WRAP_TEST = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Container", + "direction": "row", + "wrap": "wrap", + "items": { + "type": "Frame", + "width": 100, + "height": 100 + }, + "data": "${Array.range(10)}" + } + } +} +)apl"; + +TEST_F(AutoSizeTest, WrapTest) { + // Fixed viewport, single line + metrics = Metrics().size(1000, 1000); + loadDocument(WRAP_TEST); + ASSERT_TRUE(checkComponent(1000, 1000)); // Auto-scale both directions, fixed viewport + ASSERT_TRUE(checkViewport(1000, 1000)); + + // Fixed viewport, two lines + metrics = Metrics().size(800, 1000); + loadDocument(WRAP_TEST); + ASSERT_TRUE(checkComponent(800, 1000)); + ASSERT_TRUE(checkViewport(800, 1000)); + + // Fixed viewport, single line, allow wrap horizontal + metrics = Metrics().size(1000, 1000).minAndMaxWidth(500, 1000); + loadDocument(WRAP_TEST); + ASSERT_TRUE(checkComponent(1000, 1000)); // Auto-scale both directions, fixed viewport + ASSERT_TRUE(checkViewport(1000, 1000)); + + // Fixed viewport, single line, allow wrap horizontal and vertical + metrics = Metrics().size(1000, 1000).minAndMaxHeight(100, 1000).minAndMaxWidth(500, 1000); + loadDocument(WRAP_TEST); + ASSERT_TRUE(checkComponent(1000, 100)); // Auto-scale both directions, fixed viewport + ASSERT_TRUE(checkViewport(1000, 100)); + + // Fixed viewport, two lines, allow wrap horizontal and vertical + metrics = Metrics().size(600, 1000).minAndMaxHeight(100, 1000).minAndMaxWidth(500, 750); + loadDocument(WRAP_TEST); + ASSERT_TRUE(checkComponent(750, 200)); // Fixes width to max width first, then height to calculated + ASSERT_TRUE(checkViewport(750, 200)); + + // Fixed viewport, multiple lines, allow wrap horizontal and vertical + metrics = Metrics().size(200, 200).minAndMaxHeight(100, 400).minAndMaxWidth(100, 400); + loadDocument(WRAP_TEST); + ASSERT_TRUE(checkComponent(400, 300)); // Fixes width to max width first, then height to calculated + ASSERT_TRUE(checkViewport(400, 300)); } + +/** + * This test has an auto-sizing frame wrapped around something of a known size. + */ static const char *EMBEDDED_TEST = R"apl( { "type": "APL", @@ -49,8 +364,10 @@ static const char *EMBEDDED_TEST = R"apl( "mainTemplate": { "item": { "type": "Frame", + "id": "OUTER", "item": { "type": "Frame", + "id": "INNER", "width": 100, "height": 200 } @@ -61,20 +378,25 @@ static const char *EMBEDDED_TEST = R"apl( TEST_F(AutoSizeTest, Embedded) { - metrics.size(300, 300).autoSizeWidth(true); + metrics = Metrics().size(300, 300); loadDocument(EMBEDDED_TEST); ASSERT_TRUE(component); - ASSERT_TRUE(IsEqual(Rect(0,0,100,300), component->getCalculated(apl::kPropertyBounds))); + ASSERT_TRUE(IsEqual(Rect(0,0,300,300), component->getCalculated(apl::kPropertyBounds))); - metrics.size(500,500).autoSizeWidth(false).autoSizeHeight(true); + metrics = Metrics().size(300, 300).minAndMaxWidth(200, 400); + loadDocument(EMBEDDED_TEST); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(Rect(0,0,200,300), component->getCalculated(apl::kPropertyBounds))); + + metrics = Metrics().size(500,500).minAndMaxHeight(100, 600); loadDocument(EMBEDDED_TEST); ASSERT_TRUE(component); ASSERT_TRUE(IsEqual(Rect(0,0,500,200), component->getCalculated(apl::kPropertyBounds))); - metrics.size(400,400).autoSizeWidth(true).autoSizeHeight(true); + metrics = Metrics().size(400,400).minAndMaxWidth(300, 500).minAndMaxHeight(350, 450); loadDocument(EMBEDDED_TEST); ASSERT_TRUE(component); - ASSERT_TRUE(IsEqual(Rect(0,0,100,200), component->getCalculated(apl::kPropertyBounds))); + ASSERT_TRUE(IsEqual(Rect(0,0,300,350), component->getCalculated(apl::kPropertyBounds))); } static const char *SCROLL_VIEW = R"apl( @@ -97,7 +419,7 @@ static const char *SCROLL_VIEW = R"apl( TEST_F(AutoSizeTest, ScrollView) { // The ScrollView defaults to an auto-sized width and a height of 100. - metrics.autoSizeWidth(true).autoSizeHeight(true); + metrics.minAndMaxWidth(200, 400).minAndMaxHeight(50, 2000); loadDocument(SCROLL_VIEW); ASSERT_TRUE(component); ASSERT_TRUE(IsEqual(Rect(0,0,300,100), component->getCalculated(apl::kPropertyBounds))); @@ -122,42 +444,315 @@ static const char *RESIZING = R"apl( } )apl"; +/** + * The Frame doesn't have any min/max, so it will take on the dimensions of the viewport + */ TEST_F(AutoSizeTest, Resizing) { - auto doInitialize = [&](const char *document, float width, float height) -> ::testing::AssertionResult { - loadDocument(document); - if (!component) - return ::testing::AssertionFailure() << "Failed to load document"; - return IsEqual(Rect(0, 0, width, height), component->getCalculated(kPropertyBounds)); - }; - - auto doTest = [&](const char *property, int value, float width, float height) -> ::testing::AssertionResult { - executeCommand("SetValue", {{"componentId", "FOO"}, {"property", property}, {"value", value}}, true); - root->clearPending(); - return IsEqual(Rect(0,0,width,height), component->getCalculated(kPropertyBounds)); - }; - // Allow resizing in both direction - metrics.size(100,200).autoSizeWidth(true).autoSizeHeight(true); - ASSERT_TRUE(doInitialize(RESIZING, 12, 22)); - ASSERT_TRUE(doTest("width", 40, 42, 22)); - ASSERT_TRUE(doTest("height", 70, 42, 72)); + metrics = Metrics().size(100,200).minAndMaxWidth(50, 1000).minAndMaxHeight(100, 900); + ASSERT_TRUE(doInitialize(RESIZING, 50, 100)); // Starts at 50,100 + ASSERT_TRUE(checkViewport(50, 100)); + ASSERT_TRUE(doTest("width", 70, 72, 100)); + ASSERT_TRUE(checkViewport(72, 100)); + ASSERT_TRUE(doTest("width", 700, 702, 100)); + ASSERT_TRUE(checkViewport(702, 100)); + ASSERT_TRUE(doTest("width", 2000, 1000, 100)); + ASSERT_TRUE(checkViewport(1000, 100)); + ASSERT_TRUE(doTest("height", 700, 1000, 702)); + ASSERT_TRUE(checkViewport(1000, 702)); + ASSERT_TRUE(doTest("height", 1000, 1000, 900)); + ASSERT_TRUE(checkViewport(1000, 900)); + ASSERT_TRUE(doTest("height", 10, 1000, 100)); + ASSERT_TRUE(checkViewport(1000, 100)); // Auto-size width - metrics.size(100,200).autoSizeWidth(true).autoSizeHeight(false); - ASSERT_TRUE(doInitialize(RESIZING, 12, 200)); - ASSERT_TRUE(doTest("width", 40, 42, 200)); - ASSERT_TRUE(doTest("height", 70, 42, 200)); + metrics = Metrics().size(100,200).minAndMaxWidth(50, 1000); + ASSERT_TRUE(doInitialize(RESIZING, 50, 200)); + ASSERT_TRUE(checkViewport(50, 200)); + ASSERT_TRUE(doTest("width", 40, 50, 200)); + ASSERT_TRUE(checkViewport(50, 200)); + ASSERT_TRUE(doTest("width", 100, 102, 200)); + ASSERT_TRUE(checkViewport(102, 200)); + ASSERT_TRUE(doTest("height", 70, 102, 200)); + ASSERT_TRUE(checkViewport(102, 200)); // Auto-size height - metrics.size(100,200).autoSizeWidth(false).autoSizeHeight(true); - ASSERT_TRUE(doInitialize(RESIZING, 100, 22)); - ASSERT_TRUE(doTest("width", 40, 100, 22)); - ASSERT_TRUE(doTest("height", 70, 100, 72)); + metrics = Metrics().size(100,200).minAndMaxHeight(100, 900); + ASSERT_TRUE(doInitialize(RESIZING, 100, 100)); + ASSERT_TRUE(checkViewport(100, 100)); + ASSERT_TRUE(doTest("width", 200, 100, 100)); + ASSERT_TRUE(checkViewport(100, 100)); + ASSERT_TRUE(doTest("height", 170, 100, 172)); + ASSERT_TRUE(checkViewport(100, 172)); // No auto-sizing - metrics.size(100,200).autoSizeWidth(false).autoSizeHeight(false); + metrics = Metrics().size(100,200); ASSERT_TRUE(doInitialize(RESIZING, 100, 200)); + ASSERT_TRUE(checkViewport(100, 200)); ASSERT_TRUE(doTest("width", 40, 100, 200)); + ASSERT_TRUE(checkViewport(100, 200)); ASSERT_TRUE(doTest("height", 70, 100, 200)); -} \ No newline at end of file + ASSERT_TRUE(checkViewport(100, 200)); +} + + +/** + * Fixed size viewport. + * Auto-sizing frame with min/max values + */ +static const char *MIN_MAX_BOUNDED_TEST = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Frame", + "width": "auto", + "minWidth": 100, + "maxWidth": 200, + "height": "auto", + "minHeight": 100, + "maxHeight": 200, + "item": { + "type": "Frame", + "id": "FOO", + "width": 125, + "height": 200 + } + } + } +} +)apl"; + +/* + * The Frame has a min/max, so it will not take on the viewport dimensions. + * The viewport has a fixed size + */ +TEST_F(AutoSizeTest, MinMaxBounded) +{ + // Allow resizing in both direction; larger viewport + metrics = Metrics().size(400,400); + ASSERT_TRUE(doInitialize(MIN_MAX_BOUNDED_TEST, 125, 200)); + ASSERT_TRUE(checkViewport(400,400)); + + // Wider than the maxWidth + ASSERT_TRUE(doTest("width", 300, 200, 200)); + ASSERT_TRUE(checkViewport(400,400)); + + // Narrower than the minWidth + ASSERT_TRUE(doTest("width", 20, 100, 200)); + ASSERT_TRUE(checkViewport(400,400)); + + // Shorter + ASSERT_TRUE(doTest("height", 20, 100, 100)); + ASSERT_TRUE(checkViewport(400,400)); + + // Taller + ASSERT_TRUE(doTest("height", 250, 100, 200)); + ASSERT_TRUE(checkViewport(400,400)); + + // Shrink width + ASSERT_TRUE(doTest("width", 150, 150, 200)); + ASSERT_TRUE(checkViewport(400,400)); + + // Switch to a viewport that's a little smaller than the max size of the frame + // The same tests result in the same basic size because the component has a maxWidth/Height, + // and hence the component width/height is calculated and clamped to the min/max Width/Height. + metrics = Metrics().size(150,150); + ASSERT_TRUE(doInitialize(MIN_MAX_BOUNDED_TEST, 125, 200)); + ASSERT_TRUE(checkViewport(150,150)); + + // Wider than the maxWidth + ASSERT_TRUE(doTest("width", 300, 200, 200)); + ASSERT_TRUE(checkViewport(150,150)); + + // Narrower than the minWidth + ASSERT_TRUE(doTest("width", 20, 100, 200)); + ASSERT_TRUE(checkViewport(150,150)); + + // Shorter + ASSERT_TRUE(doTest("height", 20, 100, 100)); + ASSERT_TRUE(checkViewport(150,150)); + + // Taller + ASSERT_TRUE(doTest("height", 250, 100, 200)); + ASSERT_TRUE(checkViewport(150,150)); + + // Shrink width + ASSERT_TRUE(doTest("width", 150, 150, 200)); + ASSERT_TRUE(checkViewport(150,150)); +} + +/** + * Bounded with max/min width. + */ +static const char *MIN_MAX_VARIABLE_TEST = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Frame", + "width": "auto", + "minWidth": 100, + "maxWidth": 200, + "height": "auto", + "minHeight": 100, + "maxHeight": 200, + "item": { + "type": "Frame", + "id": "FOO", + "width": 125, + "height": 200 + } + } + } +} +)apl"; + +/* + * The Frame has a min/max, so it will not take on the viewport dimensions. + * The viewport also has a min/max, so it will stretch/shrink to match the frame (to a point) + */ +TEST_F(AutoSizeTest, MinMaxVariable) +{ + // Allow resizing in both direction; larger viewport + metrics = Metrics().size(400,400).minAndMaxWidth(150, 500).minAndMaxHeight(150,500); + ASSERT_TRUE(doInitialize(MIN_MAX_VARIABLE_TEST, 125, 200)); // Clamps to viewport.minWidth + ASSERT_TRUE(checkViewport(150,200)); + + // Wider than the maxWidth + ASSERT_TRUE(doTest("width", 300, 200, 200)); // Component clamps to 200 + ASSERT_TRUE(checkViewport(200,200)); + + // Narrower than the minWidth + ASSERT_TRUE(doTest("width", 20, 100, 200)); + ASSERT_TRUE(checkViewport(150,200)); + + // Shorter + ASSERT_TRUE(doTest("height", 20, 100, 100)); + ASSERT_TRUE(checkViewport(150,150)); + + // Taller + ASSERT_TRUE(doTest("height", 250, 100, 200)); + ASSERT_TRUE(checkViewport(150,200)); + + // Widen width + ASSERT_TRUE(doTest("width", 150, 150, 200)); + ASSERT_TRUE(checkViewport(150,200)); + + // Smaller viewport that will clamp _before_ the component min/max + metrics = Metrics().size(400,400).minAndMaxWidth(125, 175).minAndMaxHeight(125,175); + ASSERT_TRUE(doInitialize(MIN_MAX_VARIABLE_TEST, 125, 200)); // Clamps to viewport.minWidth + ASSERT_TRUE(checkViewport(125,175)); // The viewport has been clamped (the component leaks out a bit) + + // Wider than the maxWidth + ASSERT_TRUE(doTest("width", 300, 200, 200)); // Component clamps to 200 + ASSERT_TRUE(checkViewport(175,175)); // Viewport clamps smaller + + // Narrower than the minWidth + ASSERT_TRUE(doTest("width", 20, 100, 200)); + ASSERT_TRUE(checkViewport(125,175)); + + // Shorter + ASSERT_TRUE(doTest("height", 20, 100, 100)); + ASSERT_TRUE(checkViewport(125,125)); + + // Taller + ASSERT_TRUE(doTest("height", 250, 100, 200)); + ASSERT_TRUE(checkViewport(125,175)); + + // Widen width + ASSERT_TRUE(doTest("width", 150, 150, 200)); + ASSERT_TRUE(checkViewport(150,175)); +} + +/** + * Configuration change. + */ +static const char *CONFIGURATION_CHANGE_TEST = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Frame", + "width": "auto", + "minWidth": 100, + "maxWidth": 200, + "height": "auto", + "minHeight": 100, + "maxHeight": 200, + "item": { + "type": "Frame", + "id": "FOO", + "width": 125, + "height": 200 + } + } + } +} +)apl"; + +TEST_F(AutoSizeTest, ConfigurationChange) +{ + // Allow resizing in both direction; larger viewport + // DPI=320 -> width 400dp, height 400dp, minWidth 150dp, maxWidth 500dp, minHeight 150dp, maxHeight 500dp + metrics = Metrics().dpi(320).size(800, 800).minAndMaxWidth(300, 1000).minAndMaxHeight(300, 1000); + ASSERT_TRUE(doInitialize(CONFIGURATION_CHANGE_TEST, 125, 200)); // Inner 125x200, outer matches + ASSERT_TRUE(checkViewport(150, 200)); + + // Wider than the maxWidth + ASSERT_TRUE(doTest("width", 300, 200, 200)); // Component clamps to 200 + ASSERT_TRUE(checkViewport(200, 200)); + + // Viewport width 175, minWidth 100, maxWidth 175, height 300, minHeight 250, maxHeight 375 + auto configChange = ConfigurationChange().sizeRange(350, 200, 350, 600, 500, 750); + root->configurationChange(configChange); + root->clearPending(); + ASSERT_TRUE(checkComponent(200, 200)); // Inner frame 300x200, Outer frame 200,200 + ASSERT_TRUE(checkViewport(175, 250)); // Viewport minHeight=250, maxWidth=175 + + // Viewport width 175, minWidth 150, maxWidth 250, height 150, minHeight 150, maxHeight 175 + configChange = ConfigurationChange().sizeRange(350, 300, 500, 300, 300, 350); + root->configurationChange(configChange); + root->clearPending(); + ASSERT_TRUE(checkComponent(200, 200)); // Inner frame 300x200, Outer frame 200,200 + ASSERT_TRUE(checkViewport(200, 175)); // Viewport, maxHeight 175 +} + +static const char *TEXT_RESIZING = R"apl( +{ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "Text", + "id": "FOO", + "text": "Lorem" + } + ] + } + } +} +)apl"; + +TEST_F(AutoSizeTest, ResizingWithText) +{ + // Allow resizing in both direction + metrics = Metrics().size(100,200).minAndMaxWidth(100, 200).minAndMaxHeight(20, 200); + ASSERT_TRUE(doInitialize(TEXT_RESIZING, 100, 20)); // Starts at 100, 20 + ASSERT_TRUE(checkViewport(100, 20)); + + // Auto-size to extend the width first + ASSERT_TRUE(doTest("text", "Lorem ipsum dolor", 170, 20)); + ASSERT_TRUE(checkViewport(170, 20)); + + // Now auto-size to extend the height + ASSERT_TRUE(doTest("text", "Lorem ipsum dolor sit amet, consectetur adipiscing elit", 200, 30)); + ASSERT_TRUE(checkViewport(200, 30)); +} diff --git a/aplcore/unit/scenegraph/CMakeLists.txt b/aplcore/unit/scenegraph/CMakeLists.txt index ca62468..d88df1d 100644 --- a/aplcore/unit/scenegraph/CMakeLists.txt +++ b/aplcore/unit/scenegraph/CMakeLists.txt @@ -16,6 +16,7 @@ target_sources_local(unittest test_sg.cpp testedittext.cpp unittest_sg_accessibility.cpp + unittest_sg_base.cpp unittest_sg_edit_text.cpp unittest_sg_edit_text_config.cpp unittest_sg_filter.cpp @@ -38,4 +39,5 @@ target_sources_local(unittest unittest_sg_text.cpp unittest_sg_text_properties.cpp unittest_sg_touch.cpp + unittest_sg_video.cpp ) \ No newline at end of file diff --git a/aplcore/unit/scenegraph/test_sg.cpp b/aplcore/unit/scenegraph/test_sg.cpp index 1b2773a..ebf8fd2 100644 --- a/aplcore/unit/scenegraph/test_sg.cpp +++ b/aplcore/unit/scenegraph/test_sg.cpp @@ -756,6 +756,7 @@ IsLayer::operator()(sg::LayerPtr layer) SGASSERT(CompareBasic(layer->getInteraction(), mInteraction, "Interaction"), mMsg); SGASSERT(CheckNode(layer->content(), mContentTest), std::string("Layer Content") + mMsg); SGASSERT(CompareDebug(layer->children(), mLayerTests, "Layer Children"), mMsg); + SGASSERT(CompareBasic(layer->getCharacteristic(), mCharacteristics, "Layer Characteristics"), mMsg); // Check dirty flags SGASSERT(CompareBasic(layer->getAndClearFlags(), mDirtyFlags, "Layer Flags"), mMsg); diff --git a/aplcore/unit/scenegraph/test_sg.h b/aplcore/unit/scenegraph/test_sg.h index 69aabf9..ea36a27 100644 --- a/aplcore/unit/scenegraph/test_sg.h +++ b/aplcore/unit/scenegraph/test_sg.h @@ -288,6 +288,8 @@ class IsLayer { IsLayer& dirty(sg::Layer::FlagType flags) { mDirtyFlags = flags; return *this; } + IsLayer& characteristic(sg::Layer::CharacteristicsType flags) { mCharacteristics = flags; return *this; } + ::testing::AssertionResult operator()(sg::LayerPtr layer); private: @@ -305,6 +307,7 @@ class IsLayer { std::string mMsg; sg::Layer::FlagType mDirtyFlags = 0; sg::Layer::InteractionType mInteraction = 0; + sg::Layer::CharacteristicsType mCharacteristics = 0; }; diff --git a/aplcore/unit/scenegraph/unittest_sg_accessibility.cpp b/aplcore/unit/scenegraph/unittest_sg_accessibility.cpp index 72b2a4a..d178c9c 100644 --- a/aplcore/unit/scenegraph/unittest_sg_accessibility.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_accessibility.cpp @@ -22,6 +22,7 @@ class SGAccessibilityTest : public DocumentWrapper { public: SGAccessibilityTest() : DocumentWrapper() { config->measure(std::make_shared()); + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureDynamicAccessibilityActions); } }; @@ -62,7 +63,11 @@ TEST_F(SGAccessibilityTest, Basic) { sg, IsLayer(Rect{0, 0, 300, 100}) .vertical() - .accessibility(IsAccessibility().label("Master Scroll")) + .accessibility(IsAccessibility() + .label("Master Scroll") + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)) .child( IsLayer(Rect{0, 0, 300, 120}) .child(IsLayer(Rect{0, 0, 300, 40}) @@ -88,7 +93,12 @@ TEST_F(SGAccessibilityTest, Basic) { sg, IsLayer(Rect{0, 0, 300, 100}) .vertical() - .accessibility(IsAccessibility().label("Master Scroll")) + .accessibility( + IsAccessibility() + .label("Master Scroll") + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)) .child( IsLayer(Rect{0, 0, 300, 120}) .child(IsLayer(Rect{0, 0, 300, 40}) @@ -155,6 +165,7 @@ TEST_F(SGAccessibilityTest, Role) { .content(IsTransformNode().child(IsTextNode().text("Hello").pathOp( IsFillOp(IsColorPaint(Color::BLACK)))))) .child(IsLayer(Rect{0, 100, 100, 100}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .accessibility(IsAccessibility().role(apl::kRoleImage))))); } @@ -206,16 +217,11 @@ TEST_F(SGAccessibilityTest, Actions) sg, IsLayer(Rect{0, 0, 300, 300}) .pressable() .accessibility(IsAccessibility() - .action("activate", "Message to Server", true) - .action("deactivate", "Different message", false)))); + .action("activate", "Message to Server", true)))); // Execute the first action sg->getLayer()->getAccessibility()->executeCallback("activate"); CheckSendEvent(root, "alpha"); - - // Try to execute the second (it is disabled) - sg->getLayer()->getAccessibility()->executeCallback("deactivate"); - ASSERT_FALSE(root->hasEvent()); } @@ -318,4 +324,93 @@ TEST_F(SGAccessibilityTest, Comparisons) b->appendAction("bounce", "this is a bounce", true); a->appendAction("bounce", "this is a bounce", false); ASSERT_NE(*a, *b); +} + +static const char *NORMAL_PAGER = R"apl( + { + "type": "APL", + "version": "1.6", + "mainTemplate": { + "items": { + "type": "Pager", + "id": "MyPager", + "width": 300, + "height": 300, + "navigation": "normal", + "items": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "${data}" + }, + "data": [ + "red", + "blue", + "green" + ] + } + } + } +)apl"; + +TEST_F(SGAccessibilityTest, NormalPager) +{ + loadDocument(NORMAL_PAGER); + ASSERT_TRUE(component); + + auto sg = root->getSceneGraph(); + ASSERT_TRUE(sg); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 300, 300}, "...Pager") + .horizontal() + .child(IsLayer(Rect{0, 0, 300, 300}, "...Child1") + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 300, 300, 0)) + .pathOp(IsFillOp(IsColorPaint(Color::RED))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); + + sg->getLayer()->getAccessibility()->executeCallback(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD); + root->clearPending(); + + sg = root->getSceneGraph(); + ASSERT_TRUE(sg); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 300, 300}, "...Pager") + .horizontal() + .dirty(sg::Layer::kFlagChildrenChanged | sg::Layer::kFlagAccessibilityChanged) + .child(IsLayer(Rect{0, 0, 300, 300}, "...Child1") + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 300, 300, 0)) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); + + sg->getLayer()->getAccessibility()->executeCallback(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD); + root->clearPending(); + + sg = root->getSceneGraph(); + ASSERT_TRUE(sg); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 300, 300}, "...Pager") + .horizontal() + .dirty(sg::Layer::kFlagChildrenChanged | sg::Layer::kFlagAccessibilityChanged) + .child(IsLayer(Rect{0, 0, 300, 300}, "...Child1") + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 300, 300, 0)) + .pathOp(IsFillOp(IsColorPaint(Color::GREEN))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true)))); } \ No newline at end of file diff --git a/aplcore/unit/scenegraph/unittest_sg_base.cpp b/aplcore/unit/scenegraph/unittest_sg_base.cpp new file mode 100644 index 0000000..f924691 --- /dev/null +++ b/aplcore/unit/scenegraph/unittest_sg_base.cpp @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +#include "../testeventloop.h" +#include "test_sg.h" +#include "apl/scenegraph/builder.h" + +using namespace apl; + +class SGBaseTest : public DocumentWrapper {}; + +static const char* DEFAULT_DOC = R"({ + "type": "APL", + "version": "1.6", + "mainTemplate": { + "item": { + "type": "Frame" + } + } +})"; + +/** + * A trivial scene graph should still give the correct size + */ +TEST_F(SGBaseTest, Simple) +{ + metrics.size(200,300); + loadDocument(DEFAULT_DOC); + ASSERT_TRUE(component); + + auto sg = root->getSceneGraph(); + ASSERT_TRUE(IsEqual(Size(200, 300), sg->getViewportSize())); +} + + +static const char* MUTATING_DOC = R"({ + "type": "APL", + "version": "1.6", + "mainTemplate": { + "item": { + "type": "Frame", + "id": "TARGET", + "width": 200, + "height": 200 + } + } +})"; + +/** + * A mutating document in a variable viewport will change the viewport size + */ +TEST_F(SGBaseTest, Mutating) +{ + metrics.size(300,300).minAndMaxWidth(100,400).minAndMaxHeight(150,350); + loadDocument(MUTATING_DOC); + ASSERT_TRUE(component); + + auto sg = root->getSceneGraph(); + ASSERT_TRUE(IsEqual(Size(200, 200), sg->getViewportSize())); // Wrapped to component + + executeCommand("SetValue", {{"componentId", "TARGET"}, {"property", "width"}, {"value", 50}}, false); + sg = root->getSceneGraph(); + ASSERT_TRUE(IsEqual(Size(100, 200), sg->getViewportSize())); // Wrapped to component, but clipped to minWidth + + executeCommand("SetValue", {{"componentId", "TARGET"}, {"property", "height"}, {"value", 600}}, false); + sg = root->getSceneGraph(); + ASSERT_TRUE(IsEqual(Size(100, 350), sg->getViewportSize())); // Maxes out viewport height +} diff --git a/aplcore/unit/scenegraph/unittest_sg_frame.cpp b/aplcore/unit/scenegraph/unittest_sg_frame.cpp index c0b8a85..221789f 100644 --- a/aplcore/unit/scenegraph/unittest_sg_frame.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_frame.cpp @@ -285,6 +285,90 @@ TEST_F(SGFrameTest, ModifyFrames) { }))); } +static const char *MODIFY_GRADIENT_FRAMES = R"apl( + { + "type": "APL", + "version": "1.8", + "mainTemplate": { + "item": { + "type": "Container", + "width": 200, + "height": 100, + "id": "Container", + "items": { + "type": "Frame", + "id": "${data}Frame", + "padding": 10, + "width": 40, + "height": 50, + "backgroundColor": "${data}" + }, + "data": [ + "green", + "blue", + "red" + ] + } + } + } +)apl"; + +static const char *SWAP_TO_GRADIENT = R"apl([{ + "type": "SetValue", + "componentId": "greenFrame", + "property": "background", + "value": { + "type": "linear", + "colorRange": [ "red", "white" ], + "inputRange": [ 0, 1 ] + } +}])apl"; + +TEST_F(SGFrameTest, ModifyGradientFrames) { + loadDocument(MODIFY_GRADIENT_FRAMES); + ASSERT_TRUE(component); + + auto sg = root->getSceneGraph(); + ASSERT_TRUE(sg); + + ASSERT_TRUE(CheckSceneGraph(sg, IsLayer(Rect{0, 0, 200, 100}, "..container") + .children({ + IsLayer(Rect{0, 0, 40, 50}, "..frame1") + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 40, 50, 0)) + .pathOp(IsFillOp(IsColorPaint(Color::GREEN)))), + IsLayer(Rect{0, 50, 40, 50}, "..frame2") + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 40, 50, 0)) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))), + // Note that the third Frame is off the screen and is not drawn + }))); + + rapidjson::Document doc; + doc.Parse(SWAP_TO_GRADIENT); + executeCommands(apl::Object(doc), false); + + sg = root->getSceneGraph(); + + ASSERT_TRUE(CheckSceneGraph(sg, IsLayer(Rect{0, 0, 200, 100}, "..container") + .children({ + IsLayer(Rect{0, 0, 40, 50}, "..frame1") + .dirty(sg::Layer::kFlagRedrawContent) + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 40, 50, 0)) + .pathOp(IsFillOp(IsLinearGradientPaint({0, 1}, + {Color::RED, Color::WHITE}, + Gradient::GradientSpreadMethod::PAD, + true, {0.5, 0}, {0.5, 1}, 1, + Transform2D())))), + IsLayer(Rect{0, 50, 40, 50}, "..frame2") + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 40, 50, 0)) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))), + // Note that the third Frame is off the screen and is not drawn + }))); +} + static const char *SHADOW = R"apl( { diff --git a/aplcore/unit/scenegraph/unittest_sg_graphic.cpp b/aplcore/unit/scenegraph/unittest_sg_graphic.cpp index 466596a..982182b 100644 --- a/aplcore/unit/scenegraph/unittest_sg_graphic.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_graphic.cpp @@ -83,6 +83,7 @@ TEST_F(SGGraphicTest, BasicRectLayers) ASSERT_TRUE(CheckSceneGraph( updates, sg->layer(), IsLayer(Rect(10, 10, 90, 90)) + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .contentOffset(Point(10, 10)) .content(IsDrawNode() .path(IsGeneralPath("MLLLZ", {10, 10, 100, 10, 100, 100, 10, 100})) @@ -153,8 +154,9 @@ TEST_F(SGGraphicTest, TwoRectsLayers) updates.clear(); ASSERT_TRUE(CheckSceneGraph( updates, sg->layer(), - IsLayer(Rect(0, 0, 100, 100)).content( - IsDrawNode() + IsLayer(Rect(0, 0, 100, 100)) + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) + .content(IsDrawNode() .path(IsGeneralPath("MLLLZ", {0, 0, 100, 0, 100, 100, 0, 100})) .pathOp(IsFillOp(IsColorPaint(Color::RED))) .next(IsDrawNode() @@ -236,6 +238,7 @@ TEST_F(SGGraphicTest, ComplicatedRectLayers) ASSERT_TRUE(CheckSceneGraph( updates, sg->layer(), IsLayer(Rect(-1, -1, 102, 102)) + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .contentOffset(Point(-1,-1)) .content( IsDrawNode() @@ -438,8 +441,10 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-2.5f, -2.5f, 105, 105), "...path") // Miter limit leaves 5 unit padding + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent | sg::Layer::kFlagPositionChanged | sg::Layer::kFlagSizeChanged | sg::Layer::kFlagChildOffsetChanged) .contentOffset(Point(-2.5f, -2.5f)) @@ -455,7 +460,9 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent | sg::Layer::kFlagPositionChanged | sg::Layer::kFlagSizeChanged | sg::Layer::kFlagChildOffsetChanged) .contentOffset(Point(-0.5f, -0.5f)) @@ -471,7 +478,9 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-0.5f, -0.5f)) .childOffset(Point(-0.5f, -0.5f)) @@ -486,7 +495,9 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-0.5f, -0.5f)) .childOffset(Point(-0.5f, -0.5f)) @@ -501,7 +512,9 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-0.5f, -0.5f)) .childOffset(Point(-0.5f, -0.5f)) @@ -516,7 +529,9 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-0.5f, -0.5f)) .childOffset(Point(-0.5f, -0.5f)) @@ -531,7 +546,9 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-0.5f, -0.5f)) .childOffset(Point(-0.5f, -0.5f)) @@ -546,7 +563,9 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-0.5f, -0.5f)) .childOffset(Point(-0.5f, -0.5f)) @@ -563,7 +582,9 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-0.5f, -0.5f)) .childOffset(Point(-0.5f, -0.5f)) @@ -579,7 +600,9 @@ TEST_F(SGGraphicTest, ParameterizedLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-0.5f, -0.5f)) .childOffset(Point(-0.5f, -0.5f)) @@ -648,8 +671,9 @@ TEST_F(SGGraphicTest, BasicGroupLayers) updates.clear(); ASSERT_TRUE( CheckSceneGraph(updates, layer, - IsLayer(Rect(0,0,110,110)).content( - IsDrawNode("...draw") + IsLayer(Rect(0,0,110,110)) + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) + .content(IsDrawNode("...draw") .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) .pathOp(IsFillOp(IsColorPaint(Color::BLUE))) .next(IsDrawNode("...path2") @@ -705,6 +729,7 @@ TEST_F(SGGraphicTest, FullGroupLayer) { ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(-p, p, 2 * p, 2 * p)) + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .contentOffset(Point(-p, p)) .content(IsOpacityNode().opacity(0.5f).child( IsTransformNode() @@ -813,7 +838,9 @@ TEST_F(SGGraphicTest, ParameterizedGroupLayouts) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0,0,0,0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0,0,100,100), "...group") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .opacity(0.5f) // Opacity pulled into the layer .content(IsDrawNode() .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) @@ -824,7 +851,9 @@ TEST_F(SGGraphicTest, ParameterizedGroupLayouts) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0,0,0,0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0,0,100,100), "...group") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagOutlineChanged) .opacity(0.5f) // Opacity pulled into the layer .outline(IsGeneralPath("MLLZ", {50, 0, 100, 100, 0, 50})) @@ -837,7 +866,9 @@ TEST_F(SGGraphicTest, ParameterizedGroupLayouts) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0,0,0,0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0,0,100,100), "...group") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagTransformChanged) .opacity(0.5f) // Opacity pulled into the layer .outline(IsGeneralPath("MLLZ", {50, 0, 100, 100, 0, 50})) @@ -851,7 +882,9 @@ TEST_F(SGGraphicTest, ParameterizedGroupLayouts) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0,0,0,0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0,0,100,100), "...group") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagOpacityChanged) .opacity(1.0f) // Opacity pulled into the layer .outline(IsGeneralPath("MLLZ", {50, 0, 100, 100, 0, 50})) @@ -921,9 +954,12 @@ TEST_F(SGGraphicTest, MultiChildLayer) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0,0,0,0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child( IsLayer(Rect(0,0,0,0), "...group") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0,0,100,100), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .content(IsDrawNode() .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) .pathOp(IsFillOp(IsColorPaint(Color::BLUE, 0.5f)))))))); @@ -970,6 +1006,7 @@ TEST_F(SGGraphicTest, BasicTextLayer) ASSERT_TRUE( CheckSceneGraph(updates, layer, IsLayer(Rect(0, -8, 130, 10)) + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .contentOffset(Point(0, -8)) .childOffset(Point(0, -8)) .content(IsTransformNode() @@ -1047,6 +1084,7 @@ TEST_F(SGGraphicTest, ComplicatedTextLayer) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(-4, -12, 158, 18)) + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .contentOffset(Point(-4, -12)) .childOffset(Point(-4, -8)) .content(IsTransformNode() @@ -1212,7 +1250,9 @@ TEST_F(SGGraphicTest, ParameterizedTextLayout) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0, -8, 50, 10), "...text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent | sg::Layer::kFlagPositionChanged | sg::Layer::kFlagSizeChanged | sg::Layer::kFlagChildOffsetChanged) .contentOffset(0, -8) @@ -1226,7 +1266,9 @@ TEST_F(SGGraphicTest, ParameterizedTextLayout) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0, -8, 50, 10), "...text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(0, -8) .content(IsTransformNode() @@ -1239,7 +1281,9 @@ TEST_F(SGGraphicTest, ParameterizedTextLayout) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0, -8, 50, 10), "...text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(0, -8) .content(IsTransformNode() @@ -1255,7 +1299,9 @@ TEST_F(SGGraphicTest, ParameterizedTextLayout) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0, -8, 50, 10), "...text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(0, -8) .content(IsTransformNode() @@ -1282,7 +1328,9 @@ TEST_F(SGGraphicTest, ParameterizedTextLayout) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0, -8, 30, 10), "...text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagChildOffsetChanged | sg::Layer::kFlagRedrawContent | sg::Layer::kFlagSizeChanged | sg::Layer::kFlagPositionChanged) .contentOffset(0, -8) @@ -1295,7 +1343,9 @@ TEST_F(SGGraphicTest, ParameterizedTextLayout) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(10, -8, 30, 10), "...text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagChildOffsetChanged | sg::Layer::kFlagRedrawContent | sg::Layer::kFlagPositionChanged) .contentOffset(10, -8) @@ -1308,7 +1358,9 @@ TEST_F(SGGraphicTest, ParameterizedTextLayout) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(10, 12, 30, 10), "...text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagChildOffsetChanged | sg::Layer::kFlagRedrawContent | sg::Layer::kFlagPositionChanged) .contentOffset(10, 12) @@ -1321,7 +1373,9 @@ TEST_F(SGGraphicTest, ParameterizedTextLayout) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-20, 12, 30, 10), "...text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagChildOffsetChanged | sg::Layer::kFlagRedrawContent | sg::Layer::kFlagPositionChanged) .contentOffset(-20, 12) @@ -1443,7 +1497,9 @@ TEST_F(SGGraphicTest, ParameterizedTextStrokeLayouts) ASSERT_TRUE( CheckSceneGraph(updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-2, -10, 54, 14), "....text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-2, -10)) .content(IsTransformNode().translate(0, -8).child( @@ -1455,7 +1511,9 @@ TEST_F(SGGraphicTest, ParameterizedTextStrokeLayouts) ASSERT_TRUE( CheckSceneGraph(updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-2, -10, 54, 14), "....text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-2, -10)) .content(IsTransformNode().translate(0, -8).child( @@ -1467,7 +1525,9 @@ TEST_F(SGGraphicTest, ParameterizedTextStrokeLayouts) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-2, -10, 54, 14), "....text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-2, -10)) .content(IsTransformNode().translate(0, -8).child( @@ -1482,8 +1542,10 @@ TEST_F(SGGraphicTest, ParameterizedTextStrokeLayouts) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child( IsLayer(Rect(-2, -10, 54, 14), "....text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-2, -10)) .content(IsTransformNode().translate(0, -8).child( @@ -1499,7 +1561,9 @@ TEST_F(SGGraphicTest, ParameterizedTextStrokeLayouts) ASSERT_TRUE( CheckSceneGraph(updates, layer, IsLayer(Rect(0, 0, 0, 0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(-2, -10, 54, 14), "....text") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .contentOffset(Point(-2, -10)) .content(IsTransformNode().translate(0, -8).child( @@ -1574,7 +1638,9 @@ TEST_F(SGGraphicTest, ShadowLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0,0,0,0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(10,10,80,80), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .shadow(IsShadow(Color::BLUE, Point{3, 3}, 5)) .contentOffset(Point(10,10)) .content(IsDrawNode() @@ -1589,7 +1655,9 @@ TEST_F(SGGraphicTest, ShadowLayers) ASSERT_TRUE(CheckSceneGraph( updates, layer, IsLayer(Rect(0,0,0,0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(10,10,80,80), "...path") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagRedrawContent) .shadow(IsShadow(Color::BLUE, Point{3, 3}, 5)) .contentOffset(Point(10,10)) diff --git a/aplcore/unit/scenegraph/unittest_sg_graphic_component.cpp b/aplcore/unit/scenegraph/unittest_sg_graphic_component.cpp index cfb1fc3..60502c2 100644 --- a/aplcore/unit/scenegraph/unittest_sg_graphic_component.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_graphic_component.cpp @@ -63,6 +63,7 @@ TEST_F(SGGraphicComponentTest, Basic) sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .content(IsTransformNode(".transform") .child(IsDrawNode(".draw") .path(IsGeneralPath( @@ -75,6 +76,7 @@ TEST_F(SGGraphicComponentTest, Basic) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") .child(IsLayer(Rect{100, 0, 100, 100}, ".MediaLayer") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .dirty(sg::Layer::kFlagPositionChanged) .content(IsTransformNode().child( IsDrawNode() @@ -107,7 +109,8 @@ TEST_F(SGGraphicComponentTest, Missing) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") - .child(IsLayer(Rect{0, 0, 1, 1}, ".MediaLayer")))); // No content is visible + .child(IsLayer(Rect{0, 0, 1, 1}, ".MediaLayer") + .characteristic(sg::Layer::kCharacteristicRenderOnly)))); // No content is visible } static const char * TOGGLE_OFF_AND_ON = R"apl( @@ -160,6 +163,7 @@ TEST_F(SGGraphicComponentTest, ToggleOffAndOn) sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .content(IsTransformNode(".transform") .child(IsDrawNode(".draw") .path(IsGeneralPath( @@ -173,6 +177,7 @@ TEST_F(SGGraphicComponentTest, ToggleOffAndOn) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .dirty(sg::Layer::kFlagRedrawContent)))); // No content is visible // Set to a bad vector graphic - one with an illegal height/width @@ -182,7 +187,8 @@ TEST_F(SGGraphicComponentTest, ToggleOffAndOn) sg = root->getSceneGraph(); ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") - .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer")))); + .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer") + .characteristic(sg::Layer::kCharacteristicRenderOnly)))); // Set to an empty vector graphic - one with no content executeCommand("SetValue", @@ -190,7 +196,8 @@ TEST_F(SGGraphicComponentTest, ToggleOffAndOn) sg = root->getSceneGraph(); ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") - .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer")))); + .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer") + .characteristic(sg::Layer::kCharacteristicRenderOnly)))); // Set it back to the original executeCommand("SetValue", @@ -200,6 +207,7 @@ TEST_F(SGGraphicComponentTest, ToggleOffAndOn) sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .dirty(sg::Layer::kFlagRedrawContent) .content(IsTransformNode(".transform") .child(IsDrawNode(".draw") @@ -257,6 +265,7 @@ TEST_F(SGGraphicComponentTest, MultiText) IsLayer(Rect{0, 0, 800, 800}, "...Frame") .child(IsLayer(Rect{0, 0, 800, 800}, "...VectorGraphic") .child(IsLayer(Rect{0, 0, 800, 800}, "...Graphic") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .content(IsTransformNode() .transform(Transform2D::scale(4. / 3.)) .child(IsTransformNode() @@ -315,6 +324,7 @@ TEST_F(SGGraphicComponentTest, Moving) IsLayer(Rect{0, 0, 200, 200}) .child( IsLayer(Rect{0, 0, 200, 200}) + .characteristic(sg::Layer::kCharacteristicRenderOnly) .content(IsTransformNode(".alignment") .child(IsTransformNode(".group").child( IsDrawNode() @@ -330,6 +340,7 @@ TEST_F(SGGraphicComponentTest, Moving) IsLayer(Rect{0, 0, 200, 200}, "...vector graphic") .child( IsLayer(Rect{0, 0, 200, 200}, "...media layer") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .dirty(sg::Layer::kFlagRedrawContent) .content(IsTransformNode(".alignment") .child(IsTransformNode(".group").translate({100, 0}).child( @@ -350,8 +361,11 @@ TEST_F(SGGraphicComponentTest, MovingLayers) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}, "...vector graphic") .child(IsLayer(Rect{0, 0, 200, 200}, "...media layer") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .child(IsLayer(Rect(0,0,0,0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0,0,10,10), "...group") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .content( IsDrawNode() .path(IsGeneralPath( @@ -366,8 +380,11 @@ TEST_F(SGGraphicComponentTest, MovingLayers) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}) .child(IsLayer(Rect{0, 0, 200, 200}) + .characteristic(sg::Layer::kCharacteristicRenderOnly) .child(IsLayer(Rect(0,0,0,0), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .child(IsLayer(Rect(0,0,10,10), "...group") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .dirty(sg::Layer::kFlagTransformChanged) .transform(Transform2D::translate({100,0})) .content( @@ -428,7 +445,9 @@ TEST_F(SGGraphicComponentTest, ReplaceSource) sg, IsLayer(Rect{0, 0, 200, 200}, "...vector graphic") .child(IsLayer(Rect{0, 0, 200, 200}, "...media layer") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .child(IsLayer(Rect(0, 0, 200, 200), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .content(IsDrawNode() .path(IsGeneralPath( "MLLLZ", {0, 0, 200, 0, 200, 200, 0, 200})) @@ -443,8 +462,10 @@ TEST_F(SGGraphicComponentTest, ReplaceSource) sg, IsLayer(Rect{0, 0, 200, 200}, "...vector graphic") .child(IsLayer(Rect{0, 0, 200, 200}, "...media layer") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .dirty(sg::Layer::kFlagChildrenChanged) .child(IsLayer(Rect(0, 0, 200, 200), "...container") + .characteristic(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren) .content(IsDrawNode() .path(IsGeneralPath( "MLLLZ", {0, 0, 200, 0, 200, 200, 0, 200})) diff --git a/aplcore/unit/scenegraph/unittest_sg_graphic_loading.cpp b/aplcore/unit/scenegraph/unittest_sg_graphic_loading.cpp index 2ce248d..3358a26 100644 --- a/aplcore/unit/scenegraph/unittest_sg_graphic_loading.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_graphic_loading.cpp @@ -206,6 +206,7 @@ TEST_F(SGGraphicLoadingTest, Preloaded) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect(0, 0, 200, 200)) .child(IsLayer(Rect(0, 0, 200, 200)) + .characteristic(sg::Layer::kCharacteristicRenderOnly) .content(IsTransformNode().child( IsDrawNode() .path(IsGeneralPath("MLLLZ", {0, 0, 200, 0, 200, 200, 0, 200})) @@ -222,7 +223,8 @@ TEST_F(SGGraphicLoadingTest, Postloaded) auto sg = root->getSceneGraph(); ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect(0, 0, 200, 200)) - .child(IsLayer(Rect(0, 0, 1, 1))))); + .child(IsLayer(Rect(0, 0, 1, 1)) + .characteristic(sg::Layer::kCharacteristicRenderOnly)))); addMedia("http://bluebox", BLUE_BOX); ASSERT_TRUE(pendingMediaRequests().empty()); @@ -231,6 +233,7 @@ TEST_F(SGGraphicLoadingTest, Postloaded) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect(0, 0, 200, 200)) .child(IsLayer(Rect(0, 0, 200, 200)) + .characteristic(sg::Layer::kCharacteristicRenderOnly) .dirty(sg::Layer::kFlagRedrawContent | sg::Layer::kFlagSizeChanged) .content(IsTransformNode().child( IsDrawNode() @@ -248,7 +251,8 @@ TEST_F(SGGraphicLoadingTest, Change) // The initial VectorGraph is looking for "http://bluebox", which hasn't been received auto sg = root->getSceneGraph(); ASSERT_TRUE( - CheckSceneGraph(sg, IsLayer(Rect(0, 0, 200, 200)).child(IsLayer(Rect(0, 0, 1, 1))))); + CheckSceneGraph(sg, IsLayer(Rect(0, 0, 200, 200)).child(IsLayer(Rect(0, 0, 1, 1)) + .characteristic(sg::Layer::kCharacteristicRenderOnly)))); // Change the source to "http://redbox", add it, and verify that the VG inflates correctly executeCommand("SetValue", @@ -262,6 +266,7 @@ TEST_F(SGGraphicLoadingTest, Change) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect(0, 0, 200, 200)) .child(IsLayer(Rect(0, 0, 200, 200)) + .characteristic(sg::Layer::kCharacteristicRenderOnly) .dirty(sg::Layer::kFlagRedrawContent | sg::Layer::kFlagSizeChanged) .content(IsTransformNode().child( IsDrawNode() @@ -282,6 +287,7 @@ TEST_F(SGGraphicLoadingTest, Change) sg, IsLayer(Rect(0, 0, 200, 200)) .child(IsLayer(Rect(0, 0, 200, 200)) .dirty(sg::Layer::kFlagRedrawContent) + .characteristic(sg::Layer::kCharacteristicRenderOnly) .content(IsTransformNode().child( IsDrawNode() .path(IsGeneralPath("MLLLZ", {0, 0, 200, 0, 200, 200, 0, 200})) @@ -294,7 +300,7 @@ TEST_F(SGGraphicLoadingTest, Change) sg = root->getSceneGraph(); ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect(0, 0, 200, 200)) - .child(IsLayer(Rect(0, 0, 200, 200)).dirty(sg::Layer::kFlagRedrawContent)))); + .child(IsLayer(Rect(0, 0, 200, 200)).characteristic(sg::Layer::kCharacteristicRenderOnly).dirty(sg::Layer::kFlagRedrawContent)))); } @@ -338,7 +344,7 @@ TEST_F(SGGraphicLoadingTest, LocalGraphic) // The initial VectorGraph is looking for "http://bluebox", which hasn't been received auto sg = root->getSceneGraph(); ASSERT_TRUE( - CheckSceneGraph(sg, IsLayer(Rect(0, 0, 200, 200)).child(IsLayer(Rect(0, 0, 1, 1))))); + CheckSceneGraph(sg, IsLayer(Rect(0, 0, 200, 200)).child(IsLayer(Rect(0, 0, 1, 1)).characteristic(sg::Layer::kCharacteristicRenderOnly)))); // Change the source to "yellowBox", add it, and verify that the VG inflates correctly executeCommand("SetValue", @@ -350,6 +356,7 @@ TEST_F(SGGraphicLoadingTest, LocalGraphic) sg, IsLayer(Rect(0, 0, 200, 200)) .child(IsLayer(Rect(0, 0, 200, 200)) .dirty(sg::Layer::kFlagRedrawContent | sg::Layer::kFlagSizeChanged) + .characteristic(sg::Layer::kCharacteristicRenderOnly) .content(IsTransformNode().child( IsDrawNode() .path(IsGeneralPath("MLLLZ", {0, 0, 200, 0, 200, 200, 0, 200})) @@ -362,7 +369,7 @@ TEST_F(SGGraphicLoadingTest, LocalGraphic) sg = root->getSceneGraph(); ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect(0, 0, 200, 200)) - .child(IsLayer(Rect(0, 0, 200, 200)).dirty(sg::Layer::kFlagRedrawContent)))); + .child(IsLayer(Rect(0, 0, 200, 200)).characteristic(sg::Layer::kCharacteristicRenderOnly).dirty(sg::Layer::kFlagRedrawContent)))); // Set it back executeCommand("SetValue", @@ -373,6 +380,7 @@ TEST_F(SGGraphicLoadingTest, LocalGraphic) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect(0, 0, 200, 200)) .child(IsLayer(Rect(0, 0, 200, 200)) + .characteristic(sg::Layer::kCharacteristicRenderOnly) .dirty(sg::Layer::kFlagRedrawContent) .content(IsTransformNode().child( IsDrawNode() diff --git a/aplcore/unit/scenegraph/unittest_sg_image.cpp b/aplcore/unit/scenegraph/unittest_sg_image.cpp index 9d5aa77..af91770 100644 --- a/aplcore/unit/scenegraph/unittest_sg_image.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_image.cpp @@ -177,6 +177,7 @@ TEST_F(SGImageTest, Preloaded) { ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode() .path(IsRoundRectPath(50, 0, 100, 200, 0)) // Clip to target region .child(IsImageNode() @@ -196,6 +197,7 @@ TEST_F(SGImageTest, FailedLoad) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode() .path(IsRoundRectPath(RoundedRect{})) // Clip to target region .child(IsImageNode().filterTest(IsMediaObjectFilter( @@ -210,6 +212,7 @@ TEST_F(SGImageTest, FailedLoad) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}) .dirty(sg::Layer::kFlagRedrawContent) // The image node will have changed + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode(".clip") .path(IsRoundRectPath(RoundedRect{})) // Clip to target region .child(IsImageNode(".image") @@ -227,6 +230,7 @@ TEST_F(SGImageTest, DelayedLoad) { ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode() .path(IsRoundRectPath(RoundedRect{})) // Clip to target region .child(IsImageNode().filterTest(IsMediaObjectFilter( @@ -241,6 +245,7 @@ TEST_F(SGImageTest, DelayedLoad) { ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .dirty(sg::Layer::kFlagRedrawContent) // The image content changed .content(IsClipNode() .path(IsRoundRectPath(50, 0, 100, 200, 0)) @@ -292,6 +297,7 @@ TEST_F(SGImageTest, FramedImage) .pathOp(IsFillOp(IsColorPaint(Color::RED)))) .childClip(IsRoundRectPath(10, 10, 180, 180, 0)) .child(IsLayer(Rect{10, 10, 200, 200}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode() .path(IsRoundRectPath(50, 0, 100, 200, 0)) // Target bounds @@ -331,6 +337,7 @@ TEST_F(SGImageTest, ColorOverlay) { ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 400, 400}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode() .path(IsRoundRectPath(100, 0, 200, 400, 0)) // Clip to the image size (is this needed?) @@ -371,6 +378,7 @@ TEST_F(SGImageTest, GradientOverlay) { // Default is "center", "best-fit" ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 400, 400}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode() .path(IsRoundRectPath(100, 0, 200, 400, 0)) // Clip to the image size (is this needed?) @@ -415,6 +423,7 @@ TEST_F(SGImageTest, GradientColorOverlay) { // Default is "center", "best-fit" ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 400, 400}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode() .path(IsRoundRectPath(100, 0, 200, 400, 0)) // Clip to the image size (is this needed?) @@ -464,6 +473,7 @@ TEST_F(SGImageTest, TwoImages) { ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 250, 310}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode() .path(IsRoundRectPath(0, 130, 40, 50, 0)) // Clip to the image size (is this needed?) @@ -507,6 +517,7 @@ TEST_F(SGImageTest, TwoImagesBlend) { ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 250, 310}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content(IsClipNode() .path(IsRoundRectPath(0, 55, 100, 200, 0)) // Clip to the image size .child(IsImageNode() @@ -552,6 +563,7 @@ TEST_F(SGImageTest, ManyFilters) { ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 400, 400}) + .characteristic(sg::Layer::kCharacteristicHasMedia) .content( IsClipNode() .path(IsRoundRectPath(100, 0, 200, 400, diff --git a/aplcore/unit/scenegraph/unittest_sg_line_highlighting.cpp b/aplcore/unit/scenegraph/unittest_sg_line_highlighting.cpp index 1685c58..b3cf0db 100644 --- a/aplcore/unit/scenegraph/unittest_sg_line_highlighting.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_line_highlighting.cpp @@ -311,7 +311,14 @@ TEST_F(AudioHighlightTest, Scrolling) { .content(IsTransformNode().child( IsTextNode() .text("Line1Line2Line3Line4Line5") - .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))))); + .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); // Execute SpeakItem with line highlighting. Align the line to "first" executeCommand("SpeakItem", @@ -336,7 +343,14 @@ TEST_F(AudioHighlightTest, Scrolling) { .content(IsTransformNode().child( IsTextNode() .text("Line1Line2Line3Line4Line5") - .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))); + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); ASSERT_FALSE(factory->hasEvent()); @@ -357,7 +371,14 @@ TEST_F(AudioHighlightTest, Scrolling) { .content(IsTransformNode().child( IsTextNode() .text("Line1Line2Line3Line4Line5") - .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))); + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); // ========== The first karaoke word hits =========== advanceTime(100); @@ -379,7 +400,14 @@ TEST_F(AudioHighlightTest, Scrolling) { .range({1, 4}) .pathOp(IsFillOp(IsColorPaint(Color::RED)))) - ))))); + ))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); // ========== The second karaoke word hits. Starts scrolling Line2 =========== advanceTime(200); @@ -405,7 +433,14 @@ TEST_F(AudioHighlightTest, Scrolling) { .next(IsTextNode() .text("Line1Line2Line3Line4Line5") .range({2, 4}) - .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))))); + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); // ========== Advance past the initial scrolling but before the next word =========== advanceTime(100); @@ -431,7 +466,14 @@ TEST_F(AudioHighlightTest, Scrolling) { .next(IsTextNode() .text("Line1Line2Line3Line4Line5") .range({2, 4}) - .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))))); + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); // ========== Run off until all playback is done =========== for (int i = 0; i < 2000; i += 100) @@ -454,7 +496,14 @@ TEST_F(AudioHighlightTest, Scrolling) { .content(IsTransformNode().child( IsTextNode() .text("Line1Line2Line3Line4Line5") - .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))))); + .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); } TEST_F(AudioHighlightTest, ScrollingWithPreserve) @@ -485,7 +534,14 @@ TEST_F(AudioHighlightTest, ScrollingWithPreserve) .content(IsTransformNode().child( IsTextNode() .text("Line1Line2Line3Line4Line5") - .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))))); + .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); // Execute SpeakItem with line highlighting. Align the line to "first" executeCommand("SpeakItem", @@ -510,7 +566,14 @@ TEST_F(AudioHighlightTest, ScrollingWithPreserve) .content(IsTransformNode().child( IsTextNode() .text("Line1Line2Line3Line4Line5") - .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))); + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); ASSERT_FALSE(factory->hasEvent()); @@ -531,7 +594,14 @@ TEST_F(AudioHighlightTest, ScrollingWithPreserve) .content(IsTransformNode().child( IsTextNode() .text("Line1Line2Line3Line4Line5") - .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))); + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); // ========== The first karaoke word hits =========== advanceTime(100); @@ -553,7 +623,14 @@ TEST_F(AudioHighlightTest, ScrollingWithPreserve) .range({1, 4}) .pathOp(IsFillOp(IsColorPaint(Color::RED)))) - ))))); + ))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); // ========== The second karaoke word hits. Starts scrolling Line2 =========== advanceTime(200); @@ -579,7 +656,14 @@ TEST_F(AudioHighlightTest, ScrollingWithPreserve) .next(IsTextNode() .text("Line1Line2Line3Line4Line5") .range({2, 4}) - .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))))); + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); // ========== Advance past the initial scrolling but before the next word =========== advanceTime(100); @@ -605,7 +689,14 @@ TEST_F(AudioHighlightTest, ScrollingWithPreserve) .next(IsTextNode() .text("Line1Line2Line3Line4Line5") .range({2, 4}) - .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))))); + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); auto playerTimer = factory->getPlayers().at(0).lock()->getTimeoutId(); loop->freeze(playerTimer); @@ -634,7 +725,14 @@ TEST_F(AudioHighlightTest, ScrollingWithPreserve) .content(IsTransformNode().child( IsTextNode() .text("Line1Line2Line3Line4Line5") - .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))))); + .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); } static const char *SPEECH_MARK_HANDLER = R"apl({ diff --git a/aplcore/unit/scenegraph/unittest_sg_pager.cpp b/aplcore/unit/scenegraph/unittest_sg_pager.cpp index b190fec..0008e75 100644 --- a/aplcore/unit/scenegraph/unittest_sg_pager.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_pager.cpp @@ -88,9 +88,15 @@ TEST_F(SGPagerTest, BasicPager) .child(IsLayer(Rect{0, 0, 300, 300}, "...Child1") .content(IsDrawNode() .path(IsRoundRectPath(0, 0, 300, 300, 0)) - .pathOp(IsFillOp(IsColorPaint(Color::RED))))))); + .pathOp(IsFillOp(IsColorPaint(Color::RED))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); - // TODO: The Pager VISIBLE children properties don't get updated if you don't use touch events. auto ptr = executeCommand("AutoPage", {{"componentId", "MyPager"}, {"count", 4}, {"duration", 100}}, false); root->updateTime(100); // This should be halfway through the pager animation @@ -113,7 +119,14 @@ TEST_F(SGPagerTest, BasicPager) .transform(Transform2D::translate(150, 0)) .content(IsDrawNode() .path(IsRoundRectPath(0, 0, 300, 300, 0)) - .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))); + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); root->updateTime(250); // This should be in the pause between auto page animations sg = root->getSceneGraph(); @@ -126,5 +139,106 @@ TEST_F(SGPagerTest, BasicPager) .dirty(sg::Layer::kFlagTransformChanged) // Transform changed .content(IsDrawNode() .path(IsRoundRectPath(0, 0, 300, 300, 0)) - .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))); + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); +} + +static const char *NORMAL_PAGER = R"apl( + { + "type": "APL", + "version": "1.6", + "mainTemplate": { + "items": { + "type": "Pager", + "id": "MyPager", + "width": 300, + "height": 300, + "items": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "${data}" + }, + "data": [ + "red", + "blue", + "green" + ] + } + } + } +)apl"; + +TEST_F(SGPagerTest, NormalPager) +{ + loadDocument(NORMAL_PAGER); + ASSERT_TRUE(component); + + auto sg = root->getSceneGraph(); + ASSERT_TRUE(sg); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 300, 300}, "...Pager") + .horizontal() + .child(IsLayer(Rect{0, 0, 300, 300}, "...Child1") + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 300, 300, 0)) + .pathOp(IsFillOp(IsColorPaint(Color::RED))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); + + executeCommand("SetPage", {{"componentId", "MyPager"}, {"position", "relative"}, {"value", 1}}, false); + advanceTime(1000); + + sg = root->getSceneGraph(); + ASSERT_TRUE(sg); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 300, 300}, "...Pager") + .horizontal() + .dirty(sg::Layer::kFlagChildrenChanged) + .child(IsLayer(Rect{0, 0, 300, 300}, "...Child1") + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 300, 300, 0)) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); + + executeCommand("SetPage", {{"componentId", "MyPager"}, {"position", "relative"}, {"value", 1}}, false); + advanceTime(1000); + + sg = root->getSceneGraph(); + ASSERT_TRUE(sg); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 300, 300}, "...Pager") + .horizontal() + .dirty(sg::Layer::kFlagChildrenChanged) + .child(IsLayer(Rect{0, 0, 300, 300}, "...Child1") + .content(IsDrawNode() + .path(IsRoundRectPath(0, 0, 300, 300, 0)) + .pathOp(IsFillOp(IsColorPaint(Color::GREEN))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true)))); } \ No newline at end of file diff --git a/aplcore/unit/scenegraph/unittest_sg_pathparser.cpp b/aplcore/unit/scenegraph/unittest_sg_pathparser.cpp index 8066d52..84f5b60 100644 --- a/aplcore/unit/scenegraph/unittest_sg_pathparser.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_pathparser.cpp @@ -158,6 +158,7 @@ TEST_F(SGPathParserTest, Path) IsLayer(Rect{0, 0, 1024, 800}, "..VectorGraphic") .child( IsLayer(Rect{112, 0, 800, 800}, "...Graphic") + .characteristic(sg::Layer::kCharacteristicRenderOnly) .content( IsTransformNode() .transform(Transform2D::scale(2)) @@ -228,6 +229,7 @@ TEST_F(SGPathParserTest, Pattern) IsLayer(Rect{0, 0, 800, 800}, "..VectorGraphic") .child( IsLayer(Rect{0, 0, 800, 800}) + .characteristic(sg::Layer::kCharacteristicRenderOnly) .content( IsTransformNode() .transform(Transform2D::scale(20)) diff --git a/aplcore/unit/scenegraph/unittest_sg_text.cpp b/aplcore/unit/scenegraph/unittest_sg_text.cpp index 6c36bec..e8420f3 100644 --- a/aplcore/unit/scenegraph/unittest_sg_text.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_text.cpp @@ -335,7 +335,14 @@ TEST_F(SGTextTest, Packing) IsTextNode() .text( "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") - .pathOp(IsFillOp(IsColorPaint(Color::RED))))))) + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))) + .accessibility(IsAccessibility() + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLBACKWARD, + true) + .action(AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + AccessibilityAction::ACCESSIBILITY_ACTION_SCROLLFORWARD, + true))) .child(IsLayer(Rect{0, 460, 500, 40}) .content(IsTransformNode().child( IsTextNode() diff --git a/aplcore/unit/scenegraph/unittest_sg_touch.cpp b/aplcore/unit/scenegraph/unittest_sg_touch.cpp index 606a2f1..f09881c 100644 --- a/aplcore/unit/scenegraph/unittest_sg_touch.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_touch.cpp @@ -76,7 +76,7 @@ TEST_F(SGTouchTest, TouchWrapper) // Mouse down ASSERT_TRUE(MouseDown(root, 50, 50)); - ASSERT_TRUE(CheckDirtyDoNotClear(frame, kPropertyBackgroundColor, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirtyDoNotClear(frame, kPropertyBackgroundColor, kPropertyBackground, kPropertyVisualHash)); ASSERT_TRUE(CheckDirtyDoNotClear(root, frame)); sg = root->getSceneGraph(); @@ -95,7 +95,7 @@ TEST_F(SGTouchTest, TouchWrapper) // Mouse up ASSERT_TRUE(MouseUp(root, 60, 60)); - ASSERT_TRUE(CheckDirtyDoNotClear(frame, kPropertyBackgroundColor, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirtyDoNotClear(frame, kPropertyBackgroundColor, kPropertyBackground, kPropertyVisualHash)); ASSERT_TRUE(CheckDirtyDoNotClear(root, frame)); sg = root->getSceneGraph(); diff --git a/aplcore/unit/scenegraph/unittest_sg_video.cpp b/aplcore/unit/scenegraph/unittest_sg_video.cpp new file mode 100644 index 0000000..d475777 --- /dev/null +++ b/aplcore/unit/scenegraph/unittest_sg_video.cpp @@ -0,0 +1,63 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "../testeventloop.h" +#include "test_sg.h" +#include "apl/scenegraph/builder.h" +#include "apl/media/mediaobject.h" +#include "../media/testmediaplayerfactory.h" + +using namespace apl; + +class SGVideoTest : public DocumentWrapper { +public: + SGVideoTest() : DocumentWrapper() { + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureManageMediaRequests); + auto mediaPlayerFactory = std::make_shared(); + config->mediaPlayerFactory(mediaPlayerFactory); + } +}; + +static const char * BASIC_TEST = R"apl( + { + "type": "APL", + "version": "1.6", + "mainTemplate": { + "items": { + "width": 100, + "height": 100, + "type": "Video", + "source": "http://fake.url" + } + } + } +)apl"; + +TEST_F(SGVideoTest, LayerCharacteristicTest) { + metrics.size(300, 300); + loadDocument(BASIC_TEST); + ASSERT_TRUE(component); + + auto sg = root->getSceneGraph(); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 100, 100}) + .characteristic(sg::Layer::kCharacteristicHasMedia) + .content(IsVideoNode() + .url("http://fake.url") + .scale(apl::kVideoScaleBestFit) + .target(Rect{0,0,100,100})) + )); +} \ No newline at end of file diff --git a/aplcore/unit/test_comparisons.h b/aplcore/unit/test_comparisons.h index 27d6e39..2b94000 100644 --- a/aplcore/unit/test_comparisons.h +++ b/aplcore/unit/test_comparisons.h @@ -59,6 +59,16 @@ IsEqual(const Point& lhs, const Point& rhs, float epsilon = 0.0001) { return ::testing::AssertionSuccess(); } +inline ::testing::AssertionResult +IsEqual(const Size& lhs, const Size& rhs, float epsilon = 0.0001) { + if (std::abs(lhs.getWidth() - rhs.getWidth()) > epsilon || + std::abs(lhs.getHeight() - rhs.getHeight()) > epsilon) + return ::testing::AssertionFailure() + << lhs.toDebugString() << " != " << rhs.toDebugString(); + + return ::testing::AssertionSuccess(); +} + inline ::testing::AssertionResult IsEqual(const Rect& lhs, const Rect& rhs, float epsilon = 0.0001) { if (std::abs(lhs.getX() - rhs.getX()) > epsilon || diff --git a/aplcore/unit/testeventloop.cpp b/aplcore/unit/testeventloop.cpp index a7de5a0..9489a38 100644 --- a/aplcore/unit/testeventloop.cpp +++ b/aplcore/unit/testeventloop.cpp @@ -95,7 +95,10 @@ getMemoryCounterMap() { } #endif - +/** + * Replicates (as closely as possible) process used in viewhosts for text measurement, but with + * every symbol being 10x10 square. Doesn't account for line breaks. + */ LayoutSize SimpleTextMeasurement::measure(Component *component, float width, @@ -103,47 +106,53 @@ SimpleTextMeasurement::measure(Component *component, float height, MeasureMode heightMode) { - auto len = component->getCalculated(kPropertyText).asString().size(); - float w = len * 10; - float h = len ? 10 : 0; - auto working_width = 10 * std::floor(width / 10); - + auto len = component->getCalculated(kPropertyText).asString().size(); // Number of characters + float singleLineWidth = static_cast(len) * 10; + float resultingWidth = 0; + auto workingWidth = 10 * std::floor(width / 10); // width clamped to symbol size + + // There are 3 MeasureModes: + // 1. Exactly - text should fit into provided metric, requested metric is reported as resulting + // measurement. + // 2. AtMost - text should fit into provided metric, but can take less, actual text size + // reported as resulting measurement. + // 3. Undefined - text is unbound in provided axis. Effectively AtMost with INFINITE or + // UNDEFINED limit (NaN in case of Yoga), actual text size reported as + // resulting measurement. switch (widthMode) { case MeasureMode::Exactly: - if (w > working_width) { - if (working_width > 0) - h = 10 * std::ceil(w / working_width); - else - h = 0; // Can't lay out text - } - w = width; + resultingWidth = width; break; case MeasureMode::AtMost: - if (w > working_width) { - if (working_width > 0) - h = 10 * std::ceil(w / working_width); - else - h = 0; // Can't lay out text - w = working_width; - } + resultingWidth = std::min(workingWidth, singleLineWidth); break; case MeasureMode::Undefined: + resultingWidth = singleLineWidth; + // Otherwise effectively undefined/NaN and height will be calculated in the wrong way. + workingWidth = resultingWidth; break; } + auto charactersPerLine = std::min(resultingWidth, workingWidth) / 10; + + // Can't layout as line can't hold single character. + if (charactersPerLine <= 0) return {resultingWidth, 0}; + + float workingHeight = 10 * std::ceil(static_cast(len) / charactersPerLine); + float resultingHeight = 0; switch (heightMode) { case MeasureMode::Exactly: - h = height; + resultingHeight = height; break; case MeasureMode::AtMost: - if (h > height) - h = height; + resultingHeight = std::min(height, workingHeight); break; case MeasureMode::Undefined: + resultingHeight = workingHeight; break; } - return {w, h}; + return {resultingWidth, resultingHeight}; } float diff --git a/aplcore/unit/testeventloop.h b/aplcore/unit/testeventloop.h index 2c98814..1b9dfae 100644 --- a/aplcore/unit/testeventloop.h +++ b/aplcore/unit/testeventloop.h @@ -95,7 +95,7 @@ class MixinCounter { public: int getCount() const { return mMessages.size(); } - void clear() { mMessages.clear(); } + virtual void clear() { mMessages.clear(); } bool check(const std::string& msg) { auto s = msg; @@ -159,6 +159,17 @@ class TestSession : public Session, public MixinCounter { void write(const char *filename, const char *func, const char *value) override { mMessages.emplace_back(std::string(value)); } + + void write(LogCommandMessage&& message) override { + logCommandMessages.emplace_back(message); + } + + void clear() override { + logCommandMessages.clear(); + MixinCounter::clear(); + } + + std::vector logCommandMessages; }; class TestLogBridge : public LogBridge, public MixinCounter { @@ -427,14 +438,18 @@ class DocumentWrapper : public ActionWrapper { return rootDocument->executeCommands(commands, fastMode); } - ActionPtr executeCommand(const std::string& name, const std::map& values, bool fastMode) { + ActionPtr executeCommand(const DocumentContextPtr& document, const std::string& name, const std::map& values, bool fastMode) { rapidjson::Value cmd(rapidjson::kObjectType); auto& alloc = command.GetAllocator(); cmd.AddMember("type", rapidjson::Value(name.c_str(), alloc).Move(), alloc); for (auto& m : values) cmd.AddMember(rapidjson::StringRef(m.first.c_str()), m.second.serialize(alloc), alloc); command.SetArray().PushBack(cmd, alloc); - return executeCommands(command, fastMode); + return document->executeCommands(command, fastMode); + } + + ActionPtr executeCommand(const std::string& name, const std::map& values, bool fastMode) { + return executeCommand(rootDocument, name, values, fastMode); } /// Given a provenance path, return the JSON that created this @@ -466,8 +481,11 @@ class DocumentWrapper : public ActionWrapper { } protected: - void createContent(const char *document, const char *data) { - content = Content::create(document, session); + void createContent(const char *document, const char *data, bool withConfig = false) { + if (withConfig) + content = Content::create(document, session, metrics, *config); + else + content = Content::create(document, session); postCreateContent(); @@ -1164,6 +1182,18 @@ CheckSendEvent(const RootContextPtr& root, Args... args) { return ::testing::AssertionSuccess(); } +inline +::testing::AssertionResult +CheckComponent(const ComponentPtr& component, float width, float height) { + return IsEqual(Rect(0,0,width,height), component->getCalculated(kPropertyBounds)); +} + +inline +::testing::AssertionResult +CheckViewport(const RootContextPtr& root, float width, float height) { + return IsEqual(Size{width, height}, root->getViewportSize()); +} + inline ::testing::AssertionResult MouseDown(const RootContextPtr& root, const CoreComponentPtr& comp, double x, double y) { diff --git a/aplcore/unit/touch/unittest_native_gestures_scrollable.cpp b/aplcore/unit/touch/unittest_native_gestures_scrollable.cpp index dfa8282..0b47cac 100644 --- a/aplcore/unit/touch/unittest_native_gestures_scrollable.cpp +++ b/aplcore/unit/touch/unittest_native_gestures_scrollable.cpp @@ -2145,7 +2145,7 @@ TEST_F(NativeGesturesScrollableTest, ScrollTriggersScroll) // Skip ahead TWO scroll delays. The first scroll command will complete in a single step and trigger // the second scroll command, which will ALSO complete in a single step. The second scroll command // will trigger a THIRD scroll command. - auto delta = config->getScrollCommandDuration(); // How long the scroll command should take + auto delta = config->getProperty(RootProperty::kScrollCommandDuration).getDouble(); // How long the scroll command should take advanceTime(delta * 2); ASSERT_EQ(Point(0,300), component->scrollPosition()); // distance = 100% + 50% = 300 dp ASSERT_FALSE(action->isPending()); @@ -2180,7 +2180,7 @@ TEST_F(NativeGesturesScrollableTest, ScrollViewCancelNativeScrolling) ASSERT_EQ(Point(0,140), component->scrollPosition()); // Now delay until the Scroll command has finished - auto delta = config->getScrollCommandDuration(); // How long the scroll command should take + auto delta = config->getProperty(RootProperty::kScrollCommandDuration).getDouble(); // How long the scroll command should take advanceTime(delta); ASSERT_EQ(Point(0,240), component->scrollPosition()); diff --git a/aplcore/unit/unittest_simpletextmeasurement.cpp b/aplcore/unit/unittest_simpletextmeasurement.cpp index 7f3a80d..5634fb0 100644 --- a/aplcore/unit/unittest_simpletextmeasurement.cpp +++ b/aplcore/unit/unittest_simpletextmeasurement.cpp @@ -54,7 +54,7 @@ class SimpleText : public ::testing::Test { } public: - std::shared_ptr measure; + TextMeasurementPtr measure; ContextPtr context; Metrics metrics; RootConfig config; diff --git a/aplcore/unit/utils/unittest_path.cpp b/aplcore/unit/utils/unittest_path.cpp index d7c30d7..e0b4fdd 100644 --- a/aplcore/unit/utils/unittest_path.cpp +++ b/aplcore/unit/utils/unittest_path.cpp @@ -22,6 +22,7 @@ class PathTest : public DocumentWrapper { public: void checkPaths(const char *filename, const std::map& map) { loadDocument(filename); + advanceTime(10); checkPaths(map); } diff --git a/aplcore/unit/utils/unittest_session.cpp b/aplcore/unit/utils/unittest_session.cpp index 355f0c7..96aaec7 100644 --- a/aplcore/unit/utils/unittest_session.cpp +++ b/aplcore/unit/utils/unittest_session.cpp @@ -161,4 +161,22 @@ TEST(DefaultConsole, InvalidSessionId) auto currentId = session->getLogId(); session->setLogIdPrefix("1- +1k"); ASSERT_EQ(currentId, session->getLogId()); -} \ No newline at end of file + +} + +TEST(DefaultConsole, DefaultSessionDoesNotLogCommands) +{ + auto bridge = std::make_shared(); + LoggerFactory::instance().initialize(bridge); + + auto session = makeDefaultSession(); + + session->write(LogCommandMessage{ + "Logged from document", + LogLevel::kInfo, + Object::NULL_OBJECT(), + Object::NULL_OBJECT() + }); + + ASSERT_EQ(0, bridge->mCount); +} diff --git a/doc/core_objects.puml b/doc/core_objects.puml index 7a99977..dc2a198 100644 --- a/doc/core_objects.puml +++ b/doc/core_objects.puml @@ -1,165 +1,351 @@ @startuml -object RootConfig #CAFFBF +class RootConfig +class Context abstract TextMeasurement #CAFFBF abstract MediaManager #CAFFBF abstract MediaPlayerFactory #CAFFBF abstract AudioPlayerFactory #CAFFBF -abstract Timers #CAFFBF -Timers : timeout_id setTimeout(Runnable, apl_duration_t); -Timers : timeout_id setAnimator(Animator, apl_duration_t); -Timers : bool clearTimeout(timeout_id); - -abstract TimeManager #CAFFBF -TimeManager <|-- Timers - -object Session - -object Settings #FFC6FF -Settings : rapidJson::Value mJson - -object Component #FFADAD -Component : ContextPtr mContext; -Component : string mUniqueId; -Component : string mId; -Component : CalculatedPropertyMap mCalculated; -Component : set mDirty; -Component : unsigned int mFlags; - -object CoreComponent #FFADAD -CoreComponent : bool mInheritParentState; -CoreComponent : State mState; -CoreComponent : string mStyle; -CoreComponent : Properties mProperties; -CoreComponent : set mAssigned; -CoreComponent : vector mChildren; -CoreComponent : vector mDisplayedChildren; -CoreComponent : CoreComponentPtr mParent; -CoreComponent : YGNodeRef mYGNodeRef; -CoreComponent : Path mPath; -CoreComponent : shared_ptr mRebuilder; -CoreComponent : Size mLayoutSize; -CoreComponent : bool mDisplayedChildrenStale; -CoreComponent : sg::LayerPtr mSceneGraphLayer; -CoreComponent <|-- Component - - -object RootContextData -RootContextData : queue events; -RootContextData : queue extensionEvents; -RootContextData : set dirty; -RootContextData : set dirtyVisualContext; -RootContextData : set dirtyDatasourceContext; -RootContextData : RuntimeState mRuntimeState; -RootContextData : map mLayouts; -RootContextData : map mCommands; -RootContextData : map mGraphics; -RootContextData : Metrics mMetrics; -RootContextData : shared_ptr mStyles; -RootContextData : unique_ptr mSequencer; -RootContextData : unique_ptr mFocusManager; -RootContextData : unique_ptr mHoverManager; -RootContextData : unique_ptr mPointerManager; -RootContextData : unique_ptr mKeyboardManager; -RootContextData : unique_ptr mDataManager; -RootContextData : unique_ptr mExtensionManager; -RootContextData : unique_ptr mLayoutManager; -RootContextData : YGConfigRef mYGConfigRef; -RootContextData : TextMeasurementPtr mTextMeasurement; -RootContextData : CoreComponentPtr mTop; -RootContextData : const RootConfig mConfig; -RootContextData : int mScreenLockCount; -RootContextData : SettingsPtr mSettings; -RootContextData : SessionPtr mSession; -RootContextData : string mLang; -RootContextData : LayoutDirection mLayoutDirection; -RootContextData : LruCache mCachedMeasures; -RootContextData : LruCache mCachedBaselines; -RootContextData : WeakPtrSet mPendingOnMounts; -RootContextData *-- RootConfig -RootContextData *-- Session -RootContextData *-- CoreComponent -RootContextData *-- Settings - -object Context -Context : ContextPtr mParent; -Context : ContextPtr mTop; -Context : shared_ptr mCore; -Context : map mMap; -Context *-- RootContextData - -object Package #FFC6FF -Package : string mName; -Package : const JsonData mJson; -Package : vector mDependencies; - -object Content #FFC6FF -Content : SessionPtr mSession; -Content : PackagePtr mMainPackage; -Content : vector> mExtensionRequests; -Content : ObjectMapPtr mExtensionSettings; -Content : State mState; -Content : const rapidjson::Value& mMainTemplate; -Content : set mRequested; -Content : set mPending; -Content : map mLoaded; -Content : vector mOrderedDependencies; -Content : map mParameterValues; -Content : vector mMainParameters; -Content : vector mEnvironmentParameters; -Content : set mPendingParameters; -Content : vector mAllParameters; +abstract Timers #CAFFBF { + timeout_id setTimeout(Runnable, apl_duration_t); + timeout_id setAnimator(Animator, apl_duration_t); + bool clearTimeout(timeout_id); + void freeze(timeout_id); + bool rehydrate(timeout_id) +} + +abstract TimeManager #CAFFBF { + int size() const + void updateTime(apl_time_t) + apl_time_t nextTimeout() + apl_time_t currentTime() + void runPending() + void clear() + void terminate() + bool isTerminated() +} + +Timers <|-- TimeManager + +abstract Session { + void write(....) + std::string mLogId +} + +object Settings #FFC6FF { + rapidJson::Value mJson +} + +object UIDObject { + std::string mUniqueId + ContextPtr mContext + enum UIDObjectType mType +} + +UIDObject *-- Context + +'object UserData { +' void *mUserData; +'} + +object Component #FFADAD { + std::string mId; + CalculatedPropertyMap mCalculated; + std::set mDirty; + unsigned int mFlags; +} + +UIDObject <|-- Component +' UserData <|-- Component + +abstract Dependant { + Object mExpression; + std::weak_ptr mBindingContext; + BindingFunction mBindingFunction; + BoundSymbolSet mSymbols; + size_t mOrder; +} + +Dependant *.. Context + +object RecalculateTarget { + std::multimap> mUpstream; +} + +RecalculateTarget "1" *-- "N" Dependant + +object RecalculateSource { + std::multimap> mDownstream; +} + +RecalculateSource "1" *.. "N" Dependant + +object CoreComponent #FFADAD { + bool mInheritParentState; + State mState; + string mStyle; + Properties mProperties; + set mAssigned; + vector mChildren; + vector mDisplayedChildren; + CoreComponentPtr mParent; + YGNodeRef mYGNodeRef; + Path mPath; + shared_ptr mRebuilder; + Size mLayoutSize; + bool mDisplayedChildrenStale; + sg::LayerPtr mSceneGraphLayer; +} + +RecalculateTarget <|-- CoreComponent +Component <|-- CoreComponent + +abstract DocumentContext { +} + + +class ContextData { + const RootConfig mConfig; + RuntimeState mRuntimeState; + SettingsPtr mSettings; + std::string mLang; + LayoutDirection mLayoutDirection; +} + +ContextData *-- RootConfig +ContextData *-- Settings + +class ContextObject { + Object mValue; + Path mProvenance; + bool mMutable; + bool mUserWriteable; +} + +class Context { + ContextPtr mParent; + ContextPtr mTop; + ContextDataPtr mCore; + map mMap; +} + +RecalculateTarget <|-- Context +RecalculateSource <|-- Context + +Context *-- ContextData +Context "1" *-- "n" ContextObject + +object Package #FFC6FF { + string mName; + const JsonData mJson; + vector mDependencies; +} + +object Content #FFC6FF { + SessionPtr mSession; + PackagePtr mMainPackage; + vector> mExtensionRequests; + ObjectMapPtr mExtensionSettings; + State mState; + const rapidjson::Value& mMainTemplate; + set mRequested; + set mPending; + map mLoaded; + vector mOrderedDependencies; + map mParameterValues; + vector mMainParameters; + vector mEnvironmentParameters; + set mPendingParameters; + vector mAllParameters; +} + Content "1" *-- "n" Package Content *-- Session -object RootContext -RootContext : ContentPtr mContent; -RootContext : ContextPtr mContext; -RootContext : shared_ptr mCore; -RootContext : shared_ptr mTimeManager; -RootContext : apl_time_t mUTCTime; -RootContext : apl_duration_t mLocalTimeAdjustment; -RootContext : ConfigurationChange mActiveConfigurationChanges; -RootContext : DisplayState mDisplayState; -RootContext : sg::SceneGraphPtr mSceneGraph; -RootContext *-- Content -RootContext *-- Context -RootContext *-- RootContextData -RootContext *-- TimeManager - - -' RootConfig -RootConfig : ContextPtr mContext; -RootConfig : TextMeasurementPtr mTextMeasurement; -RootConfig : MediaManagerPtr mMediaManager; -RootConfig : MediaPlayerFactoryPtr mMediaPlayerFactory; -RootConfig : AudioPlayerFactoryPtr mAudioPlayerFactory; -RootConfig : shared_ptr mTimeManager; -RootConfig : shared_ptr mLocaleMethods; -RootConfig : map, pair> mDefaultComponentSize; -RootConfig : SessionPtr mSession; -RootConfig : ObjectMap mEnvironmentValues; -RootConfig : map mDataSources; -RootConfig : map mLiveObjectMap; -RootConfig : multimap mLiveDataObjectWatchersMap; -RootConfig : alexaext::ExtensionProviderPtr mExtensionProvider; -RootConfig : ExtensionMediatorPtr mExtensionMediator; -RootConfig : ObjectMap mSupportedExtensions; // URI -> config -RootConfig : ObjectMap mExtensionFlags; // URI -> opaque flags -RootConfig : vector mExtensionHandlers; -RootConfig : vector mExtensionCommands; -RootConfig : vector mExtensionFilters; -RootConfig : vector mExtensionComponentDefinitions; -RootConfig : map mDefaultThemeFontColor; -RootConfig : map mDefaultThemeHighlightColor; -RootConfig : APLVersion mEnforcedAPLVersion = APLVersion::kAPLVersionIgnore; -RootConfig : vector> mHeaderFilters; -RootConfig : set mEnabledExperimentalFeatures; -RootConfig : RootPropertyMap mProperties; -' RootConfig *-- Context : Only used for pre-evaluation +abstract RootContext { + void configurationChange(const ConfigurationChange& change) + void updateDisplayState(DisplayState displayState) + void reinflate() + void clearPending() const + bool hasEvent() const + Event popEvent() + Context& context() const + ContextPtr contextPtr() const + ComponentPtr topComponent() const + DocumentContextPtr topDocument() const + bool isDirty() const + const std::set& getDirty() + void clearDirty() {} + bool isVisualContextDirty() const + void clearVisualContextDirty() + rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator) + bool isDataSourceContextDirty() const + void clearDataSourceContextDirty() + rapidjson::Value serializeDataSourceContext(rapidjson::Document::AllocatorType& allocator) + rapidjson::Value serializeDOM(bool extended, rapidjson::Document::AllocatorType& allocator) + rapidjson::Value serializeContext(rapidjson::Document::AllocatorType& allocator) + void cancelExecution() + void updateTime(apl_time_t elapsedTime) + void updateTime(apl_time_t elapsedTime, apl_time_t utcTime) + void setLocalTimeAdjustment(apl_duration_t adjustment) + void scrollToRectInComponent(const ComponentPtr& component, const Rect &bounds, + CommandScrollAlign align) + apl_time_t nextTime() + apl_time_t currentTime() const + bool screenLock() const + const RootConfig& rootConfig() const + Info info() const + + bool handlePointerEvent(const PointerEvent& pointerEvent) + bool handleKeyboard(KeyHandlerType type, const Keyboard &keyboard) + const RootConfig& getRootConfig() const + ComponentPtr findComponentById(const std::string& id) const + UIDObject* findByUniqueId(const std::string& uid) const + std::string getTheme() const + + std::map getFocusableAreas() + bool setFocus(FocusDirection direction, const Rect& origin, const std::string& targetId) + bool nextFocus(FocusDirection direction, const Rect& origin) + bool nextFocus(FocusDirection direction) + void clearFocus() + std::string getFocused() + void mediaLoaded(const std::string& source) + void mediaLoadFailed(const std::string& source, int errorCode, const std::string& error) + sg::SceneGraphPtr getSceneGraph() +} +' UserData <|-- RootContext + +class SharedContextData { + std::string mRequestedVersion; + std::unique_ptr mDocumentRegistrar; + std::unique_ptr mFocusManager; + std::unique_ptr mHoverManager; + std::unique_ptr mPointerManager; + std::unique_ptr mKeyboardManager; + std::unique_ptr mLayoutManager; + std::unique_ptr mTickScheduler; + std::unique_ptr mDirtyComponents; + std::unique_ptr mUniqueIdGenerator; + std::unique_ptr mEventManager; + std::unique_ptr mDependantManager; + const DocumentManagerPtr mDocumentManager; + std::shared_ptr mTimeManager; + std::shared_ptr mMediaManager; + std::shared_ptr mMediaPlayerFactory; + YGConfigRef mYGConfigRef; + TextMeasurementPtr mTextMeasurement; + int mScreenLockCount + LruCache mCachedMeasures; + LruCache mCachedBaselines; + std::unique_ptr mTextPropertiesCache; +} + +class LayoutManager { + const CoreRootContext& mRoot; + std::set mPendingLayout; + ViewportSize mConfiguredSize; + bool mTerminated = false; + bool mInLayout = false; // Guard against recursive calls to layout + bool mNeedToReProcessLayoutChanges = false; + std::map mPostProcess; // Collection of elements to post-process +} + +SharedContextData *-- LayoutManager +LayoutManager *-- CoreRootContext + +class DocumentContextData { + SharedContextDataPtr mSharedData; + std::weak_ptr mDocument; + Metrics mMetrics; + std::map mLayouts; + std::map mCommands; + std::map mGraphics; + std::shared_ptr mStyles; + std::unique_ptr mSequencer; + std::unique_ptr mDataManager; + std::unique_ptr mExtensionManager; + std::unique_ptr mUniqueIdManager; + CoreComponentPtr mTop; + SessionPtr mSession; + WeakPtrSet mPendingOnMounts; + std::set mDirtyVisualContext; + std::set mDirtyDatasourceContext; + std::queue mExtensionEvents; +} + +ContextData <|-- DocumentContextData +DocumentContextData *-- SharedContextData +DocumentContextData *.. DocumentContext +DocumentContextData *-- CoreComponent +DocumentContextData *-- Session + + +class CoreDocumentContext { + ContentPtr mContent; + ContextPtr mContext; + DocumentContextDataPtr mCore; + ConfigurationChange mActiveConfigurationChanges; + DisplayState mDisplayState; +} + +DocumentContext <|-- CoreDocumentContext +CoreDocumentContext *-- DocumentContextData +CoreDocumentContext *-- Content +CoreDocumentContext *-- Context + + +class CoreRootContext { + SharedContextDataPtr mShared; + std::shared_ptr mTimeManager; + apl_time_t mUTCTime; + apl_duration_t mLocalTimeAdjustment; + DisplayState mDisplayState; + CoreDocumentContextPtr mTopDocument; + sg::SceneGraphPtr mSceneGraph; +} + +RootContext <|-- CoreRootContext + +CoreRootContext *-- SharedContextData +CoreRootContext *-- TimeManager +CoreRootContext *-- CoreDocumentContext +' CoreRootContext *-- SceneGraph + + +class RootConfig #CAFFBF { + ContextPtr mContext; + TextMeasurementPtr mTextMeasurement; + MediaManagerPtr mMediaManager; + MediaPlayerFactoryPtr mMediaPlayerFactory; + AudioPlayerFactoryPtr mAudioPlayerFactory; + shared_ptr mTimeManager; + shared_ptr mLocaleMethods; + map, pair> mDefaultComponentSize; + SessionPtr mSession; + ObjectMap mEnvironmentValues; + map mDataSources; + map mLiveObjectMap; + multimap mLiveDataObjectWatchersMap; + alexaext::ExtensionProviderPtr mExtensionProvider; + ExtensionMediatorPtr mExtensionMediator; + ObjectMap mSupportedExtensions; // URI -> config + ObjectMap mExtensionFlags; // URI -> opaque flags + vector mExtensionHandlers; + vector mExtensionCommands; + vector mExtensionFilters; + vector mExtensionComponentDefinitions; + map mDefaultThemeFontColor; + map mDefaultThemeHighlightColor; + APLVersion mEnforcedAPLVersion; + vector> mHeaderFilters; + set mEnabledExperimentalFeatures; + RootPropertyMap mProperties; +} + +' RootConfig *-- Only used for pre-evaluation RootConfig *-- Session RootConfig *-- TextMeasurement RootConfig *-- MediaManager diff --git a/extensions/alexaext/CMakeLists.txt b/extensions/alexaext/CMakeLists.txt index a631eb7..7061e44 100644 --- a/extensions/alexaext/CMakeLists.txt +++ b/extensions/alexaext/CMakeLists.txt @@ -26,6 +26,7 @@ project(AlexaExt add_library(alexaext STATIC src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp + src/APLAudioNormalizationExtension/AplAudioNormalizationExtension.cpp src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp src/APLMetricsExtension/AplMetricsExtension.cpp src/APLWebflowExtension/AplWebflowBase.cpp diff --git a/extensions/alexaext/include/alexaext/APLAudioNormalizationExtension/AplAudioNormalizationExtension.h b/extensions/alexaext/include/alexaext/APLAudioNormalizationExtension/AplAudioNormalizationExtension.h new file mode 100644 index 0000000..17de764 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLAudioNormalizationExtension/AplAudioNormalizationExtension.h @@ -0,0 +1,105 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ +#ifndef APL_APLAUDIONORMALIZATIONEXTENSION_H +#define APL_APLAUDIONORMALIZATIONEXTENSION_H + +#include +#include +#include + +#include "alexaext/extensionbase.h" + +namespace alexaext { +namespace audionormalization { + +/** + * This class listens for audio normalization changed events. + */ +class Listener { +public: + virtual ~Listener() = default; + + /** + * Notify audio normalization has been enabled. + * @param activity the activity for which audio normalization has been enabled. + */ + virtual void onAudioNormalizationEnabled(const ActivityDescriptor& activity) = 0; + + /** + * Notify audio normalization has been disabled. + * @param activity the activity for which audio normalization has been disabled. + */ + virtual void onAudioNormalizationDisabled(const ActivityDescriptor& activity) = 0; +}; + +/** + * Implementation of Audio Normalization extension. + */ +class AplAudioNormalizationExtension : public ExtensionBase { +public: + static constexpr auto URI = "aplext:audionormalization:10"; + + /** + * @return the singleton instance + */ + static std::shared_ptr getInstance(); + + AplAudioNormalizationExtension(AplAudioNormalizationExtension &&) = delete; + AplAudioNormalizationExtension(AplAudioNormalizationExtension const&) = delete; + AplAudioNormalizationExtension& operator=(AplAudioNormalizationExtension &&) = delete; + AplAudioNormalizationExtension& operator=(AplAudioNormalizationExtension const&) = delete; + + /// ExtensionBase + rapidjson::Document + createRegistration(const ActivityDescriptor &activity, + const rapidjson::Value ®istrationRequest) final; + bool invokeCommand(const ActivityDescriptor &activity, + const rapidjson::Value &command) final; + void onSessionEnded(const SessionDescriptor& session) final; + + /** + * Registers a listener to receive audio normalization commands. Multiple listeners may be + * registered to receive audio normalization commands. + * + * Listeners will be removed if they are no longer strongly referenced when sessions are ended. + * @param listener the listener to add. + */ + void registerListener(const std::shared_ptr& listener); + + /** + * Unregisters a listener from receiving audio normalization commands. + * @param listener the listener to remove. + */ + void unregisterListener(const std::shared_ptr& listener); + +private: + AplAudioNormalizationExtension() : ExtensionBase(URI) {} + + void notifyListeners(const std::function& func); + + /** + * Remove any expired listeners that never unregistered. Should be run occasionally. + */ + void cleanUp(); + +private: + std::recursive_mutex mMutex; + std::vector> mListeners; +}; + +} // namespace audionormalization +} // namespace alexaext + +#endif // APL_APLAUDIONORMALIZATIONEXTENSION_H diff --git a/extensions/alexaext/include/alexaext/alexaext.h b/extensions/alexaext/include/alexaext/alexaext.h index cbfdf51..895a9c6 100644 --- a/extensions/alexaext/include/alexaext/alexaext.h +++ b/extensions/alexaext/include/alexaext/alexaext.h @@ -47,6 +47,7 @@ #include "sessiondescriptor.h" #include "types.h" #include "APLAudioPlayerExtension/AplAudioPlayerExtension.h" +#include "APLAudioNormalizationExtension/AplAudioNormalizationExtension.h" #include "APLE2EEncryptionExtension/AplE2eEncryptionExtension.h" #include "APLWebflowExtension/AplWebflowExtension.h" #include "APLMusicAlarmExtension/AplMusicAlarmExtension.h" diff --git a/extensions/alexaext/src/APLAudioNormalizationExtension/AplAudioNormalizationExtension.cpp b/extensions/alexaext/src/APLAudioNormalizationExtension/AplAudioNormalizationExtension.cpp new file mode 100644 index 0000000..7bd7d27 --- /dev/null +++ b/extensions/alexaext/src/APLAudioNormalizationExtension/AplAudioNormalizationExtension.cpp @@ -0,0 +1,121 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ +#include "alexaext/APLAudioNormalizationExtension/AplAudioNormalizationExtension.h" + +#include + +namespace alexaext { +namespace audionormalization { + +constexpr auto COMMAND_ENABLE = "Enable"; +constexpr auto COMMAND_DISABLE = "Disable"; + +std::shared_ptr +AplAudioNormalizationExtension::getInstance() +{ + static std::shared_ptr ptr(new AplAudioNormalizationExtension()); + return ptr; +} + +rapidjson::Document +AplAudioNormalizationExtension::createRegistration(const ActivityDescriptor &activity, + const rapidjson::Value ®istrationRequest) +{ + return RegistrationSuccess(DEFAULT_SCHEMA_VERSION) + .uri(URI) + .token("") + .schema(DEFAULT_SCHEMA_VERSION, [=](ExtensionSchema& schema) { + schema.uri(URI); + schema.command(COMMAND_ENABLE, [](CommandSchema &command) { + command.allowFastMode(true); + }); + schema.command(COMMAND_DISABLE, [](CommandSchema &command) { + command.allowFastMode(true); + }); + }); +} + +bool +AplAudioNormalizationExtension::invokeCommand(const ActivityDescriptor &activity, + const rapidjson::Value &command) +{ + const std::string &name = GetWithDefault(Command::NAME(), command, ""); + if (name == COMMAND_ENABLE) { + notifyListeners([&](Listener& listener) { + listener.onAudioNormalizationEnabled(activity); + }); + return true; + } else if (name == COMMAND_DISABLE) { + notifyListeners([&](Listener& listener) { + listener.onAudioNormalizationDisabled(activity); + }); + return true; + } + + return false; +} + +void +AplAudioNormalizationExtension::notifyListeners(const std::function& func) +{ + std::lock_guard lock(mMutex); + for (const auto& weakListener : mListeners) { + if (auto listener = weakListener.lock()) { + func(*listener); + } + } + + cleanUp(); +} + +void +AplAudioNormalizationExtension::onSessionEnded(const alexaext::SessionDescriptor& session) +{ + cleanUp(); +} + +void +AplAudioNormalizationExtension::registerListener(const std::shared_ptr &listener) +{ + std::lock_guard lock(mMutex); + if (listener != nullptr) { + mListeners.push_back(listener); + } +} + +void +AplAudioNormalizationExtension::unregisterListener(const std::shared_ptr &listener) +{ + std::lock_guard lock(mMutex); + // Remove listeners that are expired or the listener to be removed + mListeners.erase(std::remove_if(mListeners.begin(), mListeners.end(), + [=](const std::weak_ptr& it) { + return it.expired() || it.lock() == listener; + }), mListeners.end()); +} + +void +AplAudioNormalizationExtension::cleanUp() +{ + std::lock_guard lock(mMutex); + // Clean up any expired listeners + mListeners.erase(std::remove_if(mListeners.begin(), mListeners.end(), + [](const std::weak_ptr& it) { + return it.expired(); + }), mListeners.end()); +} + +} // namespace audionormalization +} // namespace alexaext \ No newline at end of file diff --git a/extensions/alexaext/src/APLMetricsExtension/AplMetricsExtension.cpp b/extensions/alexaext/src/APLMetricsExtension/AplMetricsExtension.cpp index dfba0fd..3d6c844 100644 --- a/extensions/alexaext/src/APLMetricsExtension/AplMetricsExtension.cpp +++ b/extensions/alexaext/src/APLMetricsExtension/AplMetricsExtension.cpp @@ -63,14 +63,14 @@ AplMetricsExtension::createRegistration(const ActivityDescriptor& activity, return RegistrationFailure::forInvalidMessage(activity.getURI()); auto settingsObject = settings->GetObject(); - if (!settingsObject.HasMember(APPLICATION_ID)) + if (!settingsObject.HasMember(APPLICATION_ID) || !settingsObject[APPLICATION_ID].IsString()) return RegistrationFailure::forInvalidMessage(activity.getURI()); applicationId = settingsObject[APPLICATION_ID].GetString(); if (applicationId.empty()) return RegistrationFailure::forInvalidMessage(activity.getURI()); - if (settingsObject.HasMember(EXPERIENCE_ID)) + if (settingsObject.HasMember(EXPERIENCE_ID) && settingsObject[EXPERIENCE_ID].IsString()) experienceId = settingsObject[EXPERIENCE_ID].GetString(); { diff --git a/extensions/unit/CMakeLists.txt b/extensions/unit/CMakeLists.txt index e594cf1..b48448b 100644 --- a/extensions/unit/CMakeLists.txt +++ b/extensions/unit/CMakeLists.txt @@ -17,12 +17,13 @@ set(CMAKE_CXX_STANDARD 11) ## It is expected that the enclosing project provides GTest dependency. add_executable(alexaext-unittest + unittest_apl_attention_system.cpp + unittest_apl_audio_normalization.cpp unittest_apl_audio_player.cpp unittest_apl_e2e_encryption.cpp unittest_apl_metric.cpp unittest_apl_webflow.cpp unittest_apl_music_alarm.cpp - unittest_apl_attention_system.cpp unittest_activity_descriptor.cpp unittest_extension_lifecycle.cpp unittest_extension_message.cpp diff --git a/extensions/unit/unittest_apl_audio_normalization.cpp b/extensions/unit/unittest_apl_audio_normalization.cpp new file mode 100644 index 0000000..caaf28f --- /dev/null +++ b/extensions/unit/unittest_apl_audio_normalization.cpp @@ -0,0 +1,216 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ +#include + +#include + +#include "alexaext/APLAudioNormalizationExtension/AplAudioNormalizationExtension.h" + +namespace alexaext { +namespace audionormalization { + +static auto sListenerCreateCount = 0; +static auto sListenerDestroyCount = 0; + +class TestListener : public Listener { +public: + TestListener() { sListenerCreateCount++; } + + TestListener(const TestListener& listener) { sListenerCreateCount++; } + + ~TestListener() override { sListenerDestroyCount++; } + + void onAudioNormalizationEnabled(const ActivityDescriptor& activity) override { + auto it = mState.find(activity); + if (it != mState.end()) { + it->second = true; + } else { + mState.emplace(activity, true); + } + }; + + void onAudioNormalizationDisabled(const ActivityDescriptor& activity) override { + auto it = mState.find(activity); + if (it != mState.end()) { + it->second = false; + } else { + mState.emplace(activity, false); + } + } + +public: + std::unordered_map mState; +}; + + +class AplAudioNormalizationTest : public ::testing::Test { +public: + void TearDown() override { + testListener.reset(); + ASSERT_EQ(sListenerCreateCount, sListenerDestroyCount); + } + +public: + ActivityDescriptorPtr createActivity() { + return ActivityDescriptor::create(AplAudioNormalizationExtension::URI, SessionDescriptor::create()); + } + + std::shared_ptr extension = AplAudioNormalizationExtension::getInstance(); + + std::shared_ptr testListener = std::make_shared(); + + ActivityDescriptorPtr activity = createActivity(); + + Command enable() { return Command("1.0").name("Enable"); } + + Command disable() { return Command("1.0").name("Disable"); } +}; + +TEST_F(AplAudioNormalizationTest, TestRegister) +{ + auto registerSuccess = extension->createRegistration(*createActivity(), RegistrationRequest("1.0").uri(AplAudioNormalizationExtension::URI)); + ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registerSuccess, "")); + ASSERT_STREQ(AplAudioNormalizationExtension::URI, GetWithDefault(RegistrationSuccess::URI(), registerSuccess, "")); + + rapidjson::Value *schema = RegistrationSuccess::SCHEMA().Get(registerSuccess); + ASSERT_TRUE(schema); + + rapidjson::Value *commands = ExtensionSchema::COMMANDS().Get(*schema); + ASSERT_TRUE(commands); + + auto expectedCommandSet = std::set{ "Enable", "Disable" }; + ASSERT_TRUE(commands->IsArray() && commands->Size() == expectedCommandSet.size()); + for (const rapidjson::Value &com : commands->GetArray()) { + ASSERT_TRUE(com.IsObject()); + auto name = GetWithDefault(Command::NAME(), com, "MissingName"); + ASSERT_TRUE(expectedCommandSet.count(name) == 1) << "Unknown Command:" << name; + expectedCommandSet.erase(name); + } + ASSERT_TRUE(expectedCommandSet.empty()); +} + +TEST_F(AplAudioNormalizationTest, TestCommands) +{ + extension->registerListener(testListener); + + extension->invokeCommand(*activity, enable()); + ASSERT_TRUE(testListener->mState.at(*activity)); + + extension->invokeCommand(*activity, disable()); + ASSERT_FALSE(testListener->mState.at(*activity)); + + extension->unregisterListener(testListener); +} + +TEST_F(AplAudioNormalizationTest, TestUnregisteredListenerNotUpdated) +{ + extension->registerListener(testListener); + + extension->invokeCommand(*activity, enable()); + ASSERT_TRUE(testListener->mState.at(*activity)); + + extension->unregisterListener(testListener); + + extension->invokeCommand(*activity, disable()); + ASSERT_TRUE(testListener->mState.at(*activity)); +} + +TEST_F(AplAudioNormalizationTest, TestMultipleListeners) +{ + auto listener2 = std::make_shared(); + extension->registerListener(testListener); + extension->registerListener(listener2); + + extension->invokeCommand(*activity, enable()); + ASSERT_TRUE(testListener->mState.at(*activity)); + ASSERT_TRUE(listener2->mState.at(*activity)); + + extension->invokeCommand(*activity, disable()); + ASSERT_FALSE(testListener->mState.at(*activity)); + ASSERT_FALSE(listener2->mState.at(*activity)); +} + +TEST_F(AplAudioNormalizationTest, MultipleListenersUnregister) +{ + auto listener2 = std::make_shared(); + extension->registerListener(testListener); + extension->registerListener(listener2); + + extension->invokeCommand(*activity, enable()); + ASSERT_TRUE(testListener->mState.at(*activity)); + ASSERT_TRUE(listener2->mState.at(*activity)); + + extension->unregisterListener(listener2); + + extension->invokeCommand(*activity, disable()); + ASSERT_FALSE(testListener->mState.at(*activity)); + ASSERT_TRUE(listener2->mState.at(*activity)); +} + +TEST_F(AplAudioNormalizationTest, TestMultipleListenersMultipleActivities) +{ + auto activity2 = createActivity(); + auto listener2 = std::make_shared(); + + extension->registerListener(testListener); + extension->registerListener(listener2); + + extension->invokeCommand(*activity2, enable()); + ASSERT_TRUE(testListener->mState.at(*activity2)); + ASSERT_TRUE(listener2->mState.at(*activity2)); + + extension->invokeCommand(*activity2, disable()); + ASSERT_FALSE(testListener->mState.at(*activity2)); + ASSERT_FALSE(listener2->mState.at(*activity2)); + + extension->invokeCommand(*activity, enable()); + ASSERT_TRUE(testListener->mState.at(*activity)); + ASSERT_TRUE(listener2->mState.at(*activity)); +} + +TEST_F(AplAudioNormalizationTest, NullListenerDoesntCrash) +{ + extension->registerListener(testListener); + + testListener.reset(); + + extension->invokeCommand(*activity, enable()); +} + +TEST_F(AplAudioNormalizationTest, NullListenersRemovedOnSessionEnded) +{ + extension->registerListener(testListener); + + testListener.reset(); + + extension->onSessionEnded(*activity->getSession()); +} + +TEST_F(AplAudioNormalizationTest, NeverRegisteredListenerDoesntThrow) +{ + extension->unregisterListener(testListener); +} + +TEST_F(AplAudioNormalizationTest, NullListenerNotRegistered) +{ + extension->registerListener(nullptr); + extension->registerListener(testListener); + + extension->invokeCommand(*activity, enable()); + ASSERT_TRUE(testListener->mState.at(*activity)); +} + +} // namespace audionormalization +} // namespace alexaext \ No newline at end of file diff --git a/extensions/unit/unittest_apl_metric.cpp b/extensions/unit/unittest_apl_metric.cpp index b89bf47..c90bd00 100644 --- a/extensions/unit/unittest_apl_metric.cpp +++ b/extensions/unit/unittest_apl_metric.cpp @@ -168,7 +168,7 @@ TEST_F(AplMetricsExtensionTest, RegistrationWithoutApplicationId) { GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); } -TEST_F(AplMetricsExtensionTest, RegistrationWithoutEmptyApplicationId) { +TEST_F(AplMetricsExtensionTest, RegistrationWithEmptyApplicationId) { Document settings(kObjectType); settings.AddMember("applicationId", "", settings.GetAllocator()); settings.AddMember("experienceId", "TestExperience", settings.GetAllocator()); @@ -182,6 +182,20 @@ TEST_F(AplMetricsExtensionTest, RegistrationWithoutEmptyApplicationId) { GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); } +TEST_F(AplMetricsExtensionTest, RegistrationWithNullApplicationId) { + Document settings(kObjectType); + settings.AddMember("applicationId", Value(), settings.GetAllocator()); + settings.AddMember("experienceId", "TestExperience", settings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(settings); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + TEST_F(AplMetricsExtensionTest, RegistrationWithoutExperienceId) { Document settings(kObjectType); settings.AddMember("applicationId", "TestApplication", settings.GetAllocator()); @@ -209,6 +223,20 @@ TEST_F(AplMetricsExtensionTest, RegistrationWithEmptyExperienceId) { GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); } +TEST_F(AplMetricsExtensionTest, RegistrationWithNullExperienceId) { + Document settings(kObjectType); + settings.AddMember("applicationId", "TestApplication", settings.GetAllocator()); + settings.AddMember("experienceId", Value(), settings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(settings); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + TEST_F(AplMetricsExtensionTest, RegistrationCommands) { Document settings(kObjectType); diff --git a/test/parseDirective.cpp b/test/parseDirective.cpp index 828f5fc..b31fd32 100644 --- a/test/parseDirective.cpp +++ b/test/parseDirective.cpp @@ -260,7 +260,7 @@ main(int argc, char *argv[]) { tree.Accept(writer); } } - catch (std::exception e) { + catch (const std::exception& e) { std::cerr << "Parse error!" << e.what() << std::endl; exit(1); } diff --git a/test/utils.h b/test/utils.h index 55dd054..1b07a3f 100644 --- a/test/utils.h +++ b/test/utils.h @@ -369,7 +369,7 @@ createContext(std::vector& args, const ViewportSettings& settings) } // Parse resources - auto content = apl::Content::create(loadFile(args[0])); + auto content = apl::Content::create(loadFile(args[0]), apl::makeDefaultSession()); if (!content) { std::cerr << "Content pointer is empty" << std::endl; exit(1);