| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_SPECIFICATION_H_ |
| #define COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_SPECIFICATION_H_ |
| |
| #include <functional> |
| #include <initializer_list> |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "base/containers/flat_tree.h" |
| #include "base/feature_list.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/memory/raw_ptr.h" |
| #include "components/user_education/common/help_bubble_params.h" |
| #include "components/user_education/common/tutorial_identifier.h" |
| #include "components/user_education/common/user_education_metadata.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "ui/base/accelerators/accelerator.h" |
| #include "ui/base/interaction/element_identifier.h" |
| #include "ui/base/interaction/element_tracker.h" |
| |
| namespace gfx { |
| struct VectorIcon; |
| } |
| |
| namespace user_education { |
| |
| class FeaturePromoHandle; |
| |
| // Specifies the parameters for a feature promo and its associated bubble. |
| class FeaturePromoSpecification { |
| public: |
| // Represents additional conditions that can affect when a promo can show. |
| class AdditionalConditions { |
| public: |
| AdditionalConditions(); |
| AdditionalConditions(AdditionalConditions&&) noexcept; |
| AdditionalConditions& operator=(AdditionalConditions&&) noexcept; |
| ~AdditionalConditions(); |
| |
| // Provides constraints on when the promo can show based on some other |
| // Feautre Engagement event. |
| enum class Constraint { kAtMost, kAtLeast, kExactly }; |
| |
| // Represents an additional condition for the promo to show. |
| struct AdditionalCondition { |
| // The associated event name. |
| std::string event_name; |
| // How `count` should be interpreted. |
| Constraint constraint = Constraint::kAtMost; |
| // The required count for `event_name`, interpreted by `constraint`. |
| uint32_t count = 0; |
| // The window in which to evaluate `count` using `constraint`. |
| std::optional<uint32_t> in_days; |
| }; |
| |
| // Sets the number of days in which "used" and other events should be |
| // collected before deciding whether to show a promo. |
| // |
| // Default is zero unless there are additional conditions, in which case it |
| // is a week. |
| void set_initial_delay_days(uint32_t initial_delay_days) { |
| this->initial_delay_days_ = initial_delay_days; |
| } |
| std::optional<uint32_t> initial_delay_days() const { |
| return initial_delay_days_; |
| } |
| |
| // Sets the number of times a promoted feature can be used before the |
| // associated promo stops showing. Default is zero - i.e. if the feature is |
| // used at all, the promo won't show. |
| void set_used_limit(uint32_t used_limit) { this->used_limit_ = used_limit; } |
| std::optional<uint32_t> used_limit() const { return used_limit_; } |
| |
| // Adds an additional constraint on when the promo can show. `event_name` is |
| // arbitrary and can be shared between promos. |
| // |
| // Will only allow the promo to show if `event_name` has been seen |
| // `constraint` `count` times in `in_days` days. If `in_days` isn't |
| // specified, the period is effectively unlimited. |
| void AddAdditionalCondition(const char* event_name, |
| Constraint constraint, |
| uint32_t count, |
| std::optional<uint32_t> in_days = std::nullopt); |
| void AddAdditionalCondition( |
| const AdditionalCondition& additional_condition); |
| const std::vector<AdditionalCondition>& additional_conditions() const { |
| return additional_conditions_; |
| } |
| |
| private: |
| std::optional<uint32_t> initial_delay_days_; |
| std::optional<uint32_t> used_limit_; |
| std::vector<AdditionalCondition> additional_conditions_; |
| }; |
| |
| // Provide different ways to specify parameters for title or body text. |
| struct NoSubstitution {}; |
| using StringSubstitutions = std::vector<std::u16string>; |
| using FormatParameters = absl::variant< |
| // No substitutions; use the string as-is (default). |
| NoSubstitution, |
| // Use the following substitutions for the various substitution fields. |
| StringSubstitutions, |
| // Use a single string substitution. Included for convenience. |
| std::u16string, |
| // Specify a number of items in a singular/plural string. |
| int>; |
| |
| // Optional method that filters a set of potential `elements` to choose and |
| // return the anchor element, or null if none of the inputs is appropriate. |
| // This method can return an element different from the input list, or null |
| // if no valid element is found (this will cause the IPH not to run). |
| using AnchorElementFilter = base::RepeatingCallback<ui::TrackedElement*( |
| const ui::ElementTracker::ElementList& elements)>; |
| |
| // The callback type when creating a custom action IPH. The parameters are |
| // `context`, which provides the context of the window in which the promo was |
| // shown, and `promo_handle`, which holds the promo open until it is |
| // destroyed. |
| // |
| // Typically, if you are taking an additional sequence of actions in response |
| // to the custom callback, you will want to move `promo_handle` into longer- |
| // term storage until that sequence is complete, to prevent additional IPH or |
| // similar promos from being able to trigger in the interim. If you do not |
| // care, simply let `promo_handle` expire at the end of the callback. |
| using CustomActionCallback = |
| base::RepeatingCallback<void(ui::ElementContext context, |
| FeaturePromoHandle promo_handle)>; |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| // |
| // Describes the type of promo. Used to configure defaults for the promo's |
| // bubble. |
| enum class PromoType { |
| // Uninitialized/invalid specification. |
| kUnspecified = 0, |
| // A toast-style promo. |
| kToast = 1, |
| // A snooze-style promo. |
| kSnooze = 2, |
| // A tutorial promo. |
| kTutorial = 3, |
| // A promo where one button is replaced by a custom action. |
| kCustomAction = 4, |
| // A simple promo that acts like a toast but without the required |
| // accessibility data. |
| kLegacy = 5, |
| kMaxValue = kLegacy |
| }; |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| // |
| // Specifies the subtype of promo. Almost all promos will be `kNormal`; using |
| // some of the other special types requires being on an allowlist. |
| enum class PromoSubtype { |
| // A normal promo. Follows the default rules for when it can show. |
| kNormal = 0, |
| // A promo designed to be shown in multiple apps (or webapps). Can show once |
| // per app. |
| kPerApp = 1, |
| // A promo that must be able to be shown until explicitly acknowledged and |
| // dismissed by the user. This type requires being on an allowlist. |
| kLegalNotice = 2, |
| // A promo that must be able to be shown at most times, alerting the user |
| // that something important has happened, and offering them an opportunity |
| // to address it. This type requires being on an allowlist. |
| kActionableAlert = 3, |
| kMaxValue = kActionableAlert |
| }; |
| |
| // Represents a command or command accelerator. Can be valueless (falsy) if |
| // neither a command ID nor an explicit accelerator is specified. |
| class AcceleratorInfo { |
| public: |
| // You can assign either an int (command ID) or a ui::Accelerator to an |
| // AcceleratorInfo object. |
| using ValueType = absl::variant<int, ui::Accelerator>; |
| |
| AcceleratorInfo(); |
| AcceleratorInfo(const AcceleratorInfo& other); |
| explicit AcceleratorInfo(ValueType value); |
| AcceleratorInfo& operator=(ValueType value); |
| AcceleratorInfo& operator=(const AcceleratorInfo& other); |
| ~AcceleratorInfo(); |
| |
| explicit operator bool() const; |
| bool operator!() const { return !static_cast<bool>(*this); } |
| |
| ui::Accelerator GetAccelerator( |
| const ui::AcceleratorProvider* provider) const; |
| |
| private: |
| ValueType value_; |
| }; |
| |
| FeaturePromoSpecification(); |
| FeaturePromoSpecification(FeaturePromoSpecification&& other) noexcept; |
| FeaturePromoSpecification& operator=( |
| FeaturePromoSpecification&& other) noexcept; |
| ~FeaturePromoSpecification(); |
| |
| // Format a localized string with ID `string_id` based on the given |
| // `format_params`. |
| static std::u16string FormatString(int string_id, |
| const FormatParameters& format_params); |
| |
| // Specifies a standard toast promo. |
| // |
| // Because toasts are transient and time out after a short period, it can be |
| // difficult for screen reader users to navigate to the UI they point to. |
| // Because of this, toasts require a screen reader prompt that is different |
| // from the bubble text. This prompt should fully describe the UI the toast is |
| // pointing to, and may include a single parameter, which is the accelerator |
| // that is used to open/access the UI. |
| // |
| // For example, for a promo for the bookmark star, you might have: |
| // Bubble text: "Click here to bookmark the current tab." |
| // Accessible text: "Press |<ph name="ACCEL">$1<ex>Ctrl+D</ex></ph>| " |
| // "to bookmark the current tab" |
| // Accelerator: AcceleratorInfo(IDC_BOOKMARK_THIS_TAB) |
| // |
| // In this case, the system-specific accelerator for IDC_BOOKMARK_THIS_TAB is |
| // retrieved and its text representation is injected into the accessible text |
| // for screen reader users. An empty `AcceleratorInfo()` can be used for cases |
| // where the accessible text does not require an accelerator. |
| static FeaturePromoSpecification CreateForToastPromo( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id, |
| int accessible_text_string_id, |
| AcceleratorInfo accessible_accelerator); |
| |
| // Specifies a promo with snooze buttons. |
| static FeaturePromoSpecification CreateForSnoozePromo( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id); |
| |
| // Specifies a promo with snooze buttons, but with accessible text string id. |
| // See comments from `FeaturePromoSpecification::CreateForToastPromo()`. |
| static FeaturePromoSpecification CreateForSnoozePromo( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id, |
| int accessible_text_string_id, |
| AcceleratorInfo accessible_accelerator); |
| |
| // Specifies a promo that launches a tutorial. |
| static FeaturePromoSpecification CreateForTutorialPromo( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id, |
| TutorialIdentifier tutorial_id); |
| |
| // Specifies a promo that triggers a custom action. |
| static FeaturePromoSpecification CreateForCustomAction( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id, |
| int custom_action_string_id, |
| CustomActionCallback custom_action_callback); |
| |
| // Specifies a text-only promo without additional accessibility information. |
| // Deprecated. Only included for backwards compatibility with existing |
| // promos. This is the only case in which |feature| can be null, and if it is |
| // the result can only be used for a critical promo. |
| static FeaturePromoSpecification CreateForLegacyPromo( |
| const base::Feature* feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id); |
| |
| // Set the optional bubble title. This text appears above the body text in a |
| // slightly larger font. |
| FeaturePromoSpecification& SetBubbleTitleText(int title_text_string_id); |
| |
| // Set the optional bubble icon. This is displayed next to the title or body |
| // text. |
| FeaturePromoSpecification& SetBubbleIcon(const gfx::VectorIcon* bubble_icon); |
| |
| // Set the bubble arrow. Default is top-left. |
| FeaturePromoSpecification& SetBubbleArrow(HelpBubbleArrow bubble_arrow); |
| |
| // Overrides the default focus-on-show behavior for the bubble. By default |
| // bubbles with action buttons are focused to aid with accessibility. In |
| // unusual circumstances this allows the value to be overridden. However, it |
| // is almost always better to e.g. improve the promo trigger logic so it |
| // doesn't interrupt user workflow than it is to disable bubble auto-focus. |
| // |
| // You should document calls to this method with a reason and ideally a bug |
| // describing why the default a11y behavior needs to be overridden and what |
| // can be done to fix it. |
| FeaturePromoSpecification& OverrideFocusOnShow(bool focus_on_show); |
| |
| // Set the promo subtype. Setting the subtype to LegalNotice requires being on |
| // an allowlist. |
| FeaturePromoSpecification& SetPromoSubtype(PromoSubtype promo_subtype); |
| |
| // Set the anchor element filter. |
| FeaturePromoSpecification& SetAnchorElementFilter( |
| AnchorElementFilter anchor_element_filter); |
| |
| // Set whether we should look for the anchor element in any context. |
| // Default is false. Since usually we only want to create the bubble in the |
| // currently active window, this is only really useful for cases where there |
| // is a floating window, WebContents, or tab-modal dialog that can become |
| // detached from the current active window and therefore requires its own |
| // unique context. |
| FeaturePromoSpecification& SetInAnyContext(bool in_any_context); |
| |
| // Get the anchor element based on `anchor_element_id`, |
| // `anchor_element_filter`, and `context`. |
| ui::TrackedElement* GetAnchorElement(ui::ElementContext context) const; |
| |
| const base::Feature* feature() const { return feature_; } |
| PromoType promo_type() const { return promo_type_; } |
| PromoSubtype promo_subtype() const { return promo_subtype_; } |
| ui::ElementIdentifier anchor_element_id() const { return anchor_element_id_; } |
| const AnchorElementFilter& anchor_element_filter() const { |
| return anchor_element_filter_; |
| } |
| bool in_any_context() const { return in_any_context_; } |
| int bubble_body_string_id() const { return bubble_body_string_id_; } |
| int bubble_title_string_id() const { return bubble_title_string_id_; } |
| const gfx::VectorIcon* bubble_icon() const { return bubble_icon_; } |
| HelpBubbleArrow bubble_arrow() const { return bubble_arrow_; } |
| const std::optional<bool>& focus_on_show_override() const { |
| return focus_on_show_override_; |
| } |
| int screen_reader_string_id() const { return screen_reader_string_id_; } |
| const AcceleratorInfo& screen_reader_accelerator() const { |
| return screen_reader_accelerator_; |
| } |
| const TutorialIdentifier& tutorial_id() const { return tutorial_id_; } |
| const std::u16string custom_action_caption() const { |
| return custom_action_caption_; |
| } |
| |
| // Sets whether the custom action button is the default button on the help |
| // bubble (default is false). It is an error to call this method for a promo |
| // not created with CreateForCustomAction(). |
| FeaturePromoSpecification& SetCustomActionIsDefault( |
| bool custom_action_is_default); |
| bool custom_action_is_default() const { return custom_action_is_default_; } |
| |
| // Used to claim the callback when creating the bubble. |
| CustomActionCallback custom_action_callback() const { |
| return custom_action_callback_; |
| } |
| FeaturePromoSpecification& SetCustomActionDismissText( |
| int custom_action_dismiss_string_id); |
| int custom_action_dismiss_string_id() const { |
| return custom_action_dismiss_string_id_; |
| } |
| |
| // Set menu item element identifiers that should be highlighted while |
| // this FeaturePromo is active. |
| FeaturePromoSpecification& SetHighlightedMenuItem( |
| const ui::ElementIdentifier highlighted_menu_identifier); |
| const ui::ElementIdentifier highlighted_menu_identifier() const { |
| return highlighted_menu_identifier_; |
| } |
| |
| // Sets the additional conditions for the promo to show. |
| FeaturePromoSpecification& SetAdditionalConditions( |
| AdditionalConditions additional_conditions); |
| const AdditionalConditions& additional_conditions() const { |
| return additional_conditions_; |
| } |
| |
| // Sets the metadata for this promotion. |
| FeaturePromoSpecification& SetMetadata(Metadata metadata); |
| const Metadata& metadata() const { return metadata_; } |
| |
| // Argument-forwarding convenience version of SetMetadata() for constructing |
| // a Metadata object in-place. |
| template <typename... Args> |
| FeaturePromoSpecification& SetMetadata(Args&&... args) { |
| return SetMetadata(Metadata(std::forward<Args>(args)...)); |
| } |
| |
| // Force the subtype to a particular value, bypassing permission checks. |
| FeaturePromoSpecification& set_promo_subtype_for_testing( |
| PromoSubtype promo_subtype) { |
| promo_subtype_ = promo_subtype; |
| return *this; |
| } |
| |
| private: |
| static constexpr HelpBubbleArrow kDefaultBubbleArrow = |
| HelpBubbleArrow::kTopRight; |
| |
| FeaturePromoSpecification(const base::Feature* feature, |
| PromoType promo_type, |
| ui::ElementIdentifier anchor_element_id, |
| int bubble_body_string_id); |
| |
| raw_ptr<const base::Feature> feature_ = nullptr; |
| |
| // The type of promo. A promo with type kUnspecified is not valid. |
| PromoType promo_type_ = PromoType::kUnspecified; |
| |
| // The subtype of the promo. |
| PromoSubtype promo_subtype_ = PromoSubtype::kNormal; |
| |
| // The element identifier of the element to attach the promo to. |
| ui::ElementIdentifier anchor_element_id_; |
| |
| // Whether we are allowed to search for the anchor element in any context. |
| bool in_any_context_ = false; |
| |
| // The filter to use if there is more than one matching element, or |
| // additional processing is needed (default is to always use the first |
| // matching element). |
| AnchorElementFilter anchor_element_filter_; |
| |
| // Text to be displayed in the promo bubble body. Should not be zero for |
| // valid bubbles. We keep the string ID around because we can specify format |
| // parameters when we actually create the bubble. |
| int bubble_body_string_id_ = 0; |
| |
| // Optional text that is displayed at the top of the bubble, in a slightly |
| // more prominent font. |
| int bubble_title_string_id_ = 0; |
| |
| // Optional icon that is displayed next to bubble text. |
| raw_ptr<const gfx::VectorIcon> bubble_icon_ = nullptr; |
| |
| // Optional arrow pointing to the promo'd element. Defaults to top left. |
| HelpBubbleArrow bubble_arrow_ = kDefaultBubbleArrow; |
| |
| // Overrides the default focus-on-show behavior for a bubble, which is to |
| // focus bubbles with action buttons, but not bubbles that only have a close |
| // button. |
| std::optional<bool> focus_on_show_override_; |
| |
| // Optional screen reader announcement that replaces bubble text when the |
| // bubble is first announced. |
| int screen_reader_string_id_ = 0; |
| |
| // Accelerator that is used to fill in a parametric field in |
| // screen_reader_string_id_. |
| AcceleratorInfo screen_reader_accelerator_; |
| |
| // Tutorial identifier if the user decides to view a tutorial. |
| TutorialIdentifier tutorial_id_; |
| |
| // Custom action button text. |
| std::u16string custom_action_caption_; |
| |
| // Custom action button action. |
| CustomActionCallback custom_action_callback_; |
| |
| // Whether the custom action is the default button. |
| bool custom_action_is_default_ = false; |
| |
| // Dismiss string ID for the custom action promo. |
| int custom_action_dismiss_string_id_; |
| |
| // Identifier of the menu item that should be highlighted while |
| // FeaturePromo is active. |
| ui::ElementIdentifier highlighted_menu_identifier_; |
| |
| // Additional conditions describing when the promo can show. |
| AdditionalConditions additional_conditions_; |
| |
| // Metadata for this promo. |
| Metadata metadata_; |
| }; |
| |
| std::ostream& operator<<(std::ostream& oss, |
| FeaturePromoSpecification::PromoType promo_type); |
| std::ostream& operator<<(std::ostream& oss, |
| FeaturePromoSpecification::PromoSubtype promo_subtype); |
| |
| } // namespace user_education |
| |
| #endif // COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_SPECIFICATION_H_ |