diff --git a/flutter/shell/platform/tizen/BUILD.gn b/flutter/shell/platform/tizen/BUILD.gn index f01d55f..afe751f 100644 --- a/flutter/shell/platform/tizen/BUILD.gn +++ b/flutter/shell/platform/tizen/BUILD.gn @@ -143,12 +143,15 @@ template("embedder") { "external_texture_pixel_egl.cc", "external_texture_surface_egl.cc", "flutter_platform_node_delegate_tizen.cc", + "tizen_autofill.cc", "tizen_renderer_egl.cc", "tizen_vsync_waiter.cc", "tizen_window_ecore_wl2.cc", ] libs += [ + "capi-ui-autofill", + "capi-ui-autofill-common", "ecore_wl2", "tdm-client", "tizen-extension-client", @@ -177,12 +180,14 @@ template("embedder") { if (api_version == "6.5" && target_name != "flutter_tizen_wearable") { sources += [ "flutter_tizen_nui.cc", + "nui_autofill_popup.cc", "tizen_view_nui.cc", ] libs += [ "dali2-adaptor", "dali2-core", + "dali2-toolkit", ] defines += [ "NUI_SUPPORT" ] diff --git a/flutter/shell/platform/tizen/channels/text_input_channel.cc b/flutter/shell/platform/tizen/channels/text_input_channel.cc index 154d867..afdf29b 100644 --- a/flutter/shell/platform/tizen/channels/text_input_channel.cc +++ b/flutter/shell/platform/tizen/channels/text_input_channel.cc @@ -7,6 +7,9 @@ #include "flutter/shell/platform/common/json_method_codec.h" #include "flutter/shell/platform/tizen/flutter_tizen_engine.h" #include "flutter/shell/platform/tizen/logger.h" +#ifndef WEARABLE_PROFILE +#include "flutter/shell/platform/tizen/tizen_autofill.h" +#endif namespace flutter { @@ -23,8 +26,15 @@ constexpr char kMultilineInputType[] = "TextInputType.multiline"; constexpr char kUpdateEditingStateMethod[] = "TextInputClient.updateEditingState"; constexpr char kPerformActionMethod[] = "TextInputClient.performAction"; -constexpr char kSetPlatformViewClient[] = "TextInput.setPlatformViewClient"; +#ifndef WEARABLE_PROFILE +constexpr char kRequestAutofillMethod[] = "TextInput.requestAutofill"; +#endif +constexpr char kSetPlatformViewClientMethod[] = + "TextInput.setPlatformViewClient"; +constexpr char kSetEditableSizeAndTransformMethod[] = + "TextInput.setEditableSizeAndTransform"; constexpr char kTextCapitalization[] = "textCapitalization"; +constexpr char kTextEnableSuggestions[] = "enableSuggestions"; constexpr char kTextInputAction[] = "inputAction"; constexpr char kTextInputType[] = "inputType"; constexpr char kTextInputTypeName[] = "name"; @@ -38,8 +48,12 @@ constexpr char kSelectionBaseKey[] = "selectionBase"; constexpr char kSelectionExtentKey[] = "selectionExtent"; constexpr char kSelectionIsDirectionalKey[] = "selectionIsDirectional"; constexpr char kTextKey[] = "text"; +constexpr char kTransformKey[] = "transform"; constexpr char kBadArgumentError[] = "Bad Arguments"; constexpr char kInternalConsistencyError[] = "Internal Consistency Error"; +constexpr char kAutofill[] = "autofill"; +constexpr char kUniqueIdentifier[] = "uniqueIdentifier"; +constexpr char kHints[] = "hints"; bool IsAsciiPrintableKey(char ch) { return ch >= 32 && ch <= 126; @@ -60,6 +74,11 @@ TextInputChannel::TextInputChannel( std::unique_ptr> result) { HandleMethodCall(call, std::move(result)); }); + +#ifndef WEARABLE_PROFILE + TizenAutofill& autofill = TizenAutofill::GetInstance(); + autofill.SetOnCommit([this](std::string value) { OnCommit(value); }); +#endif } TextInputChannel::~TextInputChannel() {} @@ -136,7 +155,7 @@ void TextInputChannel::HandleMethodCall( } else if (method.compare(kHideMethod) == 0) { input_method_context_->HideInputPanel(); input_method_context_->ResetInputMethodContext(); - } else if (method.compare(kSetPlatformViewClient) == 0) { + } else if (method.compare(kSetPlatformViewClientMethod) == 0) { result->NotImplemented(); return; } else if (method.compare(kClearClientMethod) == 0) { @@ -162,11 +181,21 @@ void TextInputChannel::HandleMethodCall( } client_id_ = client_id_json.GetInt(); + + auto enable_suggestions_iter = + client_config.FindMember(kTextEnableSuggestions); + if (enable_suggestions_iter != client_config.MemberEnd() && + enable_suggestions_iter->value.IsBool()) { + bool enable = enable_suggestions_iter->value.GetBool(); + input_method_context_->SetEnableSuggestions(enable); + } + input_action_ = ""; auto input_action_iter = client_config.FindMember(kTextInputAction); if (input_action_iter != client_config.MemberEnd() && input_action_iter->value.IsString()) { input_action_ = input_action_iter->value.GetString(); + input_method_context_->SetInputAction(input_action_); } text_capitalization_ = ""; @@ -211,6 +240,27 @@ void TextInputChannel::HandleMethodCall( } } + auto autofill_iter = client_config.FindMember(kAutofill); + if (autofill_iter != client_config.MemberEnd() && + autofill_iter->value.IsObject()) { + auto unique_identifier_iter = + autofill_iter->value.FindMember(kUniqueIdentifier); + if (unique_identifier_iter != autofill_iter->value.MemberEnd() && + unique_identifier_iter->value.IsString()) { + autofill_id_ = unique_identifier_iter->value.GetString(); + } + + auto hints_iter = autofill_iter->value.FindMember(kHints); + if (hints_iter != autofill_iter->value.MemberEnd() && + hints_iter->value.IsArray()) { + autofill_hints_.clear(); + for (auto hint = hints_iter->value.GetArray().Begin(); + hint != hints_iter->value.GetArray().End(); hint++) { + autofill_hints_.push_back(hint->GetString()); + } + } + } + active_model_ = std::make_unique(); } else if (method.compare(kSetEditingStateMethod) == 0) { input_method_context_->ResetInputMethodContext(); @@ -273,6 +323,28 @@ void TextInputChannel::HandleMethodCall( cursor_offset); } SendStateUpdate(); +#ifndef WEARABLE_PROFILE + } else if (method.compare(kRequestAutofillMethod) == 0) { + TizenAutofill::GetInstance().RequestAutofill(autofill_id_, autofill_hints_); +#endif + } else if (method.compare(kSetEditableSizeAndTransformMethod) == 0) { + if (!method_call.arguments() || method_call.arguments()->IsNull()) { + result->Error(kBadArgumentError, "Method invoked without args."); + return; + } + const rapidjson::Document& args = *method_call.arguments(); + auto transform_iter = args.FindMember(kTransformKey); + if (transform_iter != args.MemberEnd() && transform_iter->value.IsArray()) { + // The 12th and 13th values of the array stores x and y values, + // respectively. + auto x_iter = transform_iter->value.GetArray().Begin() + 12; + auto y_iter = transform_iter->value.GetArray().Begin() + 13; + + InputFieldGeometry geometry; + geometry.x = x_iter->GetDouble(); + geometry.y = y_iter->GetDouble(); + input_method_context_->SetInputFieldGeometry(geometry); + } } else { result->NotImplemented(); return; diff --git a/flutter/shell/platform/tizen/channels/text_input_channel.h b/flutter/shell/platform/tizen/channels/text_input_channel.h index 11d3aa3..769a112 100644 --- a/flutter/shell/platform/tizen/channels/text_input_channel.h +++ b/flutter/shell/platform/tizen/channels/text_input_channel.h @@ -7,6 +7,7 @@ #include #include +#include #include "flutter/shell/platform/common/client_wrapper/include/flutter/binary_messenger.h" #include "flutter/shell/platform/common/client_wrapper/include/flutter/method_channel.h" @@ -79,6 +80,13 @@ class TextInputChannel { // Automatic text capitalization type. See available options: // https://api.flutter.dev/flutter/services/TextCapitalization.html std::string text_capitalization_ = ""; + + // The active autofill client id. + std::string autofill_id_; + + // A list of autofill hint strings. See available options: + // https://api.flutter.dev/flutter/services/AutofillHints-class.html + std::vector autofill_hints_; }; } // namespace flutter diff --git a/flutter/shell/platform/tizen/nui_autofill_popup.cc b/flutter/shell/platform/tizen/nui_autofill_popup.cc new file mode 100644 index 0000000..b4e68e2 --- /dev/null +++ b/flutter/shell/platform/tizen/nui_autofill_popup.cc @@ -0,0 +1,96 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/tizen/nui_autofill_popup.h" + +#include +#include + +namespace flutter { + +bool NuiAutofillPopup::Touched(Dali::Actor actor, + const Dali::TouchEvent& event) { + const Dali::PointState::Type state = event.GetState(0); + if (Dali::PointState::DOWN == state) { + std::string text = + actor.GetProperty(Dali::Actor::Property::NAME).Get(); + on_commit_(text); + popup_.SetDisplayState(Dali::Toolkit::Popup::HIDDEN); + } + return true; +} + +void NuiAutofillPopup::Hidden() { + // TODO(Swanseo0): There is a phenomenon where white traces remain for a + // while when popup disappears. + popup_.Unparent(); + popup_.Reset(); +} + +void NuiAutofillPopup::OutsideTouched() { + popup_.SetDisplayState(Dali::Toolkit::Popup::HIDDEN); +} + +Dali::Toolkit::TableView NuiAutofillPopup::MakeContent( + const std::vector>& items) { + Dali::Toolkit::TableView content = + Dali::Toolkit::TableView::New(items.size(), 1); + content.SetResizePolicy(Dali::ResizePolicy::FILL_TO_PARENT, + Dali::Dimension::ALL_DIMENSIONS); + content.SetProperty(Dali::Actor::Property::PADDING, + Dali::Vector4(10, 10, 0, 0)); + for (uint32_t i = 0; i < items.size(); ++i) { + Dali::Toolkit::TextLabel label = + Dali::Toolkit::TextLabel::New(items[i]->label); + label.SetProperty(Dali::Actor::Property::NAME, items[i]->value); + label.SetResizePolicy(Dali::ResizePolicy::DIMENSION_DEPENDENCY, + Dali::Dimension::HEIGHT); + label.SetProperty(Dali::Toolkit::TextLabel::Property::TEXT_COLOR, + Dali::Color::WHITE_SMOKE); + label.SetProperty(Dali::Toolkit::TextLabel::Property::POINT_SIZE, 7.0f); + label.TouchedSignal().Connect(this, &NuiAutofillPopup::Touched); + content.AddChild(label, Dali::Toolkit::TableView::CellPosition(i, 0)); + content.SetFitHeight(i); + } + return content; +} + +void NuiAutofillPopup::Prepare( + const std::vector>& items) { + popup_ = Dali::Toolkit::Popup::New(); + popup_.SetProperty(Dali::Actor::Property::NAME, "popup"); + popup_.SetProperty(Dali::Actor::Property::PARENT_ORIGIN, + Dali::ParentOrigin::TOP_LEFT); + popup_.SetProperty(Dali::Actor::Property::ANCHOR_POINT, + Dali::AnchorPoint::TOP_LEFT); + popup_.SetProperty(Dali::Toolkit::Popup::Property::TAIL_VISIBILITY, false); + popup_.SetBackgroundColor(Dali::Color::WHITE_SMOKE); + popup_.OutsideTouchedSignal().Connect(this, + &NuiAutofillPopup::OutsideTouched); + popup_.HiddenSignal().Connect(this, &NuiAutofillPopup::Hidden); + popup_.SetProperty(Dali::Toolkit::Popup::Property::BACKING_ENABLED, false); + popup_.SetProperty(Dali::Toolkit::Popup::Property::AUTO_HIDE_DELAY, 2500); + popup_.SetProperty(Dali::Actor::Property::SIZE, + Dali::Vector2(140.0f, 35.0f * items.size())); + + Dali::Toolkit::TableView content = MakeContent(items); + popup_.SetContent(content); +} + +void NuiAutofillPopup::Show(Dali::Actor* actor, double_t x, double_t y) { + const std::vector>& items = + TizenAutofill::GetInstance().GetResponseItems(); + if (items.empty()) { + return; + } + + Prepare(items); + + popup_.SetProperty(Dali::Actor::Property::POSITION, + Dali::Vector3(x, y, 0.5f)); + popup_.SetDisplayState(Dali::Toolkit::Popup::SHOWN); + actor->Add(popup_); +} + +} // namespace flutter diff --git a/flutter/shell/platform/tizen/nui_autofill_popup.h b/flutter/shell/platform/tizen/nui_autofill_popup.h new file mode 100644 index 0000000..4e75df7 --- /dev/null +++ b/flutter/shell/platform/tizen/nui_autofill_popup.h @@ -0,0 +1,43 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EMBEDDER_NUI_AUTOFILL_POPUP_H_ +#define EMBEDDER_NUI_AUTOFILL_POPUP_H_ + +#include +#include + +#include + +#include "flutter/shell/platform/tizen/tizen_autofill.h" + +using OnCommit = std::function; + +namespace flutter { + +class NuiAutofillPopup : public Dali::ConnectionTracker { + public: + void Show(Dali::Actor* actor, double_t x, double_t y); + + void SetOnCommit(OnCommit callback) { on_commit_ = callback; } + + private: + void Prepare(const std::vector>& items); + + void Hidden(); + + void OutsideTouched(); + + bool Touched(Dali::Actor actor, const Dali::TouchEvent& event); + + Dali::Toolkit::TableView MakeContent( + const std::vector>& items); + + Dali::Toolkit::Popup popup_; + OnCommit on_commit_; +}; + +} // namespace flutter + +#endif // EMBEDDER_NUI_AUTOFILL_POPUP_H_ diff --git a/flutter/shell/platform/tizen/tizen_autofill.cc b/flutter/shell/platform/tizen/tizen_autofill.cc new file mode 100644 index 0000000..983fdea --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_autofill.cc @@ -0,0 +1,296 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/tizen/tizen_autofill.h" + +#include +#include + +#include + +#include "flutter/shell/platform/tizen/logger.h" + +namespace flutter { + +namespace { + +std::optional ConvertAutofillHint(std::string hint) { + if (hint == "creditCardExpirationDate") { + return AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE; + } else if (hint == "creditCardExpirationDay") { + return AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY; + } else if (hint == "creditCardExpirationMonth") { + return AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH; + } else if (hint == "creditCardExpirationYear") { + return AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR; + } else if (hint == "email") { + return AUTOFILL_HINT_EMAIL_ADDRESS; + } else if (hint == "name") { + return AUTOFILL_HINT_NAME; + } else if (hint == "telephoneNumber") { + return AUTOFILL_HINT_PHONE; + } else if (hint == "postalAddress") { + return AUTOFILL_HINT_POSTAL_ADDRESS; + } else if (hint == "postalCode") { + return AUTOFILL_HINT_POSTAL_CODE; + } else if (hint == "username") { + return AUTOFILL_HINT_ID; + } else if (hint == "password") { + return AUTOFILL_HINT_PASSWORD; + } else if (hint == "creditCardSecurityCode") { + return AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE; + } + FT_LOG(Error) << "Not supported autofill hint : " << hint; + return std::nullopt; +} + +bool StoreFillResponseItem(autofill_fill_response_item_h item, + void* user_data) { + int ret = AUTOFILL_ERROR_NONE; + char* id = nullptr; + ret = autofill_fill_response_item_get_id(item, &id); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to get response item's id."; + return false; + } + + char* label = nullptr; + ret = autofill_fill_response_item_get_presentation_text(item, &label); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to get response item's presentation text."; + return false; + } + + char* value = nullptr; + ret = autofill_fill_response_item_get_value(item, &value); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to get response item's value."; + return false; + } + + std::unique_ptr response_item = + std::make_unique(); + response_item->id = std::string(id); + response_item->value = std::string(value); + response_item->label = std::string(label); + + TizenAutofill* self = static_cast(user_data); + self->StoreResponseItem(std::move(response_item)); + if (id) { + free(id); + } + + if (value) { + free(value); + } + + if (label) { + free(label); + } + return true; +} + +bool StoreForeachItem(autofill_fill_response_group_h group, void* user_data) { + autofill_fill_response_group_foreach_item(group, StoreFillResponseItem, + user_data); + return true; +}; + +void ResponseReceived(autofill_h autofill, + autofill_fill_response_h fill_response, + void* user_data) { + autofill_fill_response_foreach_group(fill_response, StoreForeachItem, + user_data); + TizenAutofill* self = static_cast(user_data); + self->OnPopup(); +}; + +autofill_save_item_h CreateSaveItem(const AutofillItem& item) { + autofill_save_item_h save_item = nullptr; + int ret = autofill_save_item_create(&save_item); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to create autofill save item."; + return nullptr; + } + + autofill_save_item_set_autofill_hint(save_item, item.hint); + autofill_save_item_set_id(save_item, item.id.c_str()); + autofill_save_item_set_label(save_item, item.label.c_str()); + autofill_save_item_set_sensitive_data(save_item, item.sensitive_data); + autofill_save_item_set_value(save_item, item.value.c_str()); + + return save_item; +} + +autofill_save_view_info_h CreateSaveViewInfo(const std::string& view_id, + const AutofillItem& item) { + autofill_save_item_h save_item = CreateSaveItem(item); + if (save_item == nullptr) { + return nullptr; + } + + char* app_id = nullptr; + int ret = app_get_id(&app_id); + if (ret != APP_ERROR_NONE) { + FT_LOG(Error) << "Faild to get app id."; + return nullptr; + } + + autofill_save_view_info_h save_view_info = nullptr; + ret = autofill_save_view_info_create(&save_view_info); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to create autofill save view info."; + return nullptr; + } + autofill_save_view_info_set_app_id(save_view_info, app_id); + autofill_save_view_info_set_view_id(save_view_info, view_id.c_str()); + autofill_save_view_info_add_item(save_view_info, save_item); + + if (app_id) { + free(app_id); + } + + return save_view_info; +} + +void AddItemsToViewInfo(autofill_view_info_h view_info, + const std::string& id, + const std::vector& hints) { + for (auto hint : hints) { + std::optional autofill_hint = ConvertAutofillHint(hint); + if (autofill_hint.has_value()) { + autofill_item_h item = nullptr; + int ret = autofill_item_create(&item); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to create autofill item."; + continue; + } + autofill_item_set_autofill_hint(item, autofill_hint.value()); + autofill_item_set_id(item, id.c_str()); + autofill_item_set_sensitive_data(item, false); + autofill_view_info_add_item(view_info, item); + autofill_item_destroy(item); + } + } +} + +autofill_view_info_h CreateViewInfo(const std::string& id, + const std::vector& hints) { + char* app_id = nullptr; + int ret = app_get_id(&app_id); + if (ret != APP_ERROR_NONE) { + FT_LOG(Error) << "Faild to get app id."; + return nullptr; + } + + autofill_view_info_h view_info = nullptr; + ret = autofill_view_info_create(&view_info); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to create autofill view info."; + return nullptr; + } + autofill_view_info_set_app_id(view_info, app_id); + autofill_view_info_set_view_id(view_info, id.c_str()); + + if (app_id) { + free(app_id); + } + + AddItemsToViewInfo(view_info, id, hints); + + return view_info; +} + +} // namespace + +TizenAutofill::TizenAutofill() { + Initialize(); +} + +TizenAutofill::~TizenAutofill() { + autofill_fill_response_unset_received_cb(autofill_); + autofill_destroy(autofill_); +} + +void TizenAutofill::Initialize() { + int ret = AUTOFILL_ERROR_NONE; + ret = autofill_create(&autofill_); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to create autofill handle."; + return; + } + + ret = autofill_connect( + autofill_, + [](autofill_h autofill, autofill_connection_status_e status, + void* user_data) { + TizenAutofill* self = static_cast(user_data); + if (status == AUTOFILL_CONNECTION_STATUS_CONNECTED) { + self->SetConnected(true); + } else { + self->SetConnected(false); + } + }, + this); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to connect to the autofill daemon."; + autofill_destroy(autofill_); + autofill_ = nullptr; + return; + } + + autofill_fill_response_set_received_cb(autofill_, ResponseReceived, this); + + response_items_.clear(); + is_initialized_ = true; +} + +void TizenAutofill::RequestAutofill(const std::string& id, + const std::vector& hints) { + if (!is_initialized_) { + return; + } + + if (!is_connected_) { + return; + } + + autofill_view_info_h view_info = CreateViewInfo(id, hints); + if (view_info == nullptr) { + return; + } + + int ret = autofill_fill_request(autofill_, view_info); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to request autofill."; + } + autofill_view_info_destroy(view_info); + + response_items_.clear(); +} + +void TizenAutofill::RegisterItem(const std::string& view_id, + const AutofillItem& item) { + if (!is_initialized_) { + return; + } + + if (!is_connected_) { + return; + } + + autofill_save_view_info_h save_view_info = CreateSaveViewInfo(view_id, item); + if (save_view_info == nullptr) { + return; + } + + int ret = autofill_commit(autofill_, save_view_info); + if (ret != AUTOFILL_ERROR_NONE) { + FT_LOG(Error) << "Failed to register autofill item."; + } + + autofill_save_view_info_destroy(save_view_info); +} + +} // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_autofill.h b/flutter/shell/platform/tizen/tizen_autofill.h new file mode 100644 index 0000000..718487a --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_autofill.h @@ -0,0 +1,84 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EMBEDDER_TIZEN_AUTOFILL_H_ +#define EMBEDDER_TIZEN_AUTOFILL_H_ + +#include +#include +#include +#include +#include + +#include + +namespace flutter { + +struct AutofillItem { + autofill_hint_e hint; + bool sensitive_data; + std::string label; + std::string id; + std::string value; +}; + +class TizenAutofill { + public: + static TizenAutofill& GetInstance() { + static TizenAutofill instance = TizenAutofill(); + return instance; + } + + void RequestAutofill(const std::string& id, + const std::vector& hints); + + void RegisterItem(const std::string& view_id, const AutofillItem& item); + + void StoreResponseItem(std::unique_ptr item) { + response_items_.push_back(std::move(item)); + } + + void SetConnected(bool connected) { is_connected_ = connected; }; + + void SetOnPopup(std::function on_popup) { on_popup_ = on_popup; } + + void SetOnCommit(std::function on_commit) { + on_commit_ = on_commit; + } + + void OnCommit(const std::string& str) { on_commit_(str); } + + void OnPopup() { + if (on_popup_) { + on_popup_(); + } + } + + const std::vector>& GetResponseItems() { + return response_items_; + } + + private: + TizenAutofill(); + + ~TizenAutofill(); + + void Initialize(); + + bool is_connected_ = false; + + bool is_initialized_ = false; + + autofill_h autofill_ = nullptr; + + std::vector> response_items_; + + std::function on_popup_; + + std::function on_commit_; +}; + +} // namespace flutter + +#endif // EMBEDDER_TIZEN_AUTOFILL_H_ diff --git a/flutter/shell/platform/tizen/tizen_input_method_context.cc b/flutter/shell/platform/tizen/tizen_input_method_context.cc index 0c94620..a0c808e 100644 --- a/flutter/shell/platform/tizen/tizen_input_method_context.cc +++ b/flutter/shell/platform/tizen/tizen_input_method_context.cc @@ -138,7 +138,6 @@ TizenInputMethodContext::TizenInputMethodContext(uintptr_t window_id) { ecore_imf_context_client_window_set(imf_context_, reinterpret_cast(window_id)); - SetContextOptions(); SetInputPanelOptions(); RegisterEventCallbacks(); } @@ -242,6 +241,15 @@ bool TizenInputMethodContext::HandleNuiKeyEvent(const char* device_name, } #endif +void TizenInputMethodContext::SetInputFieldGeometry( + InputFieldGeometry geometry) { + input_field_geometry_ = geometry; +} + +InputFieldGeometry TizenInputMethodContext::GetInputFieldGeometry() { + return input_field_geometry_; +} + InputPanelGeometry TizenInputMethodContext::GetInputPanelGeometry() { FT_ASSERT(imf_context_); InputPanelGeometry geometry; @@ -273,6 +281,39 @@ bool TizenInputMethodContext::IsInputPanelShown() { return state == ECORE_IMF_INPUT_PANEL_STATE_SHOW; } +void TizenInputMethodContext::SetEnableSuggestions(bool enable) { + FT_ASSERT(imf_context_); + ecore_imf_context_prediction_allow_set(imf_context_, + enable ? EINA_TRUE : EINA_FALSE); +} + +void TizenInputMethodContext::SetInputAction(const std::string& input_action) { + FT_ASSERT(imf_context_); + Ecore_IMF_Input_Panel_Return_Key_Type return_key_type = + ECORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_DEFAULT; + + // Not supported : none, previous, continueAction, route, emergencyCall, + // newline + if (input_action == "TextInputAction.unspecified") { + return_key_type = ECORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_DEFAULT; + } else if (input_action == "TextInputAction.done") { + return_key_type = ECORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_DONE; + } else if (input_action == "TextInputAction.go") { + return_key_type = ECORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_GO; + } else if (input_action == "TextInputAction.join") { + return_key_type = ECORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_JOIN; + } else if (input_action == "TextInputAction.next") { + return_key_type = ECORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_NEXT; + } else if (input_action == "TextInputAction.search") { + return_key_type = ECORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_SEARCH; + } else if (input_action == "TextInputAction.send") { + return_key_type = ECORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_SEND; + } + + ecore_imf_context_input_panel_return_key_type_set(imf_context_, + return_key_type); +} + void TizenInputMethodContext::SetInputPanelLayout( const std::string& input_type) { FT_ASSERT(imf_context_); @@ -387,19 +428,8 @@ void TizenInputMethodContext::UnregisterEventCallbacks() { event_callbacks_[ECORE_IMF_CALLBACK_PREEDIT_END]); } -void TizenInputMethodContext::SetContextOptions() { - FT_ASSERT(imf_context_); - ecore_imf_context_autocapital_type_set(imf_context_, - ECORE_IMF_AUTOCAPITAL_TYPE_NONE); - ecore_imf_context_prediction_allow_set(imf_context_, EINA_FALSE); -} - void TizenInputMethodContext::SetInputPanelOptions() { FT_ASSERT(imf_context_); - ecore_imf_context_input_panel_layout_set(imf_context_, - ECORE_IMF_INPUT_PANEL_LAYOUT_NORMAL); - ecore_imf_context_input_panel_return_key_type_set( - imf_context_, ECORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_DEFAULT); ecore_imf_context_input_panel_language_set( imf_context_, ECORE_IMF_INPUT_PANEL_LANG_AUTOMATIC); } diff --git a/flutter/shell/platform/tizen/tizen_input_method_context.h b/flutter/shell/platform/tizen/tizen_input_method_context.h index c241f90..829e3f4 100644 --- a/flutter/shell/platform/tizen/tizen_input_method_context.h +++ b/flutter/shell/platform/tizen/tizen_input_method_context.h @@ -13,17 +13,25 @@ #include #include +#include "flutter/shell/platform/tizen/tizen_autofill.h" + namespace flutter { -using OnCommit = std::function; -using OnPreeditChanged = std::function; +using OnCommit = std::function; +using OnPreeditChanged = + std::function; using OnPreeditStart = std::function; using OnPreeditEnd = std::function; +using OnPopupAutofillContext = std::function; struct InputPanelGeometry { int32_t x = 0, y = 0, w = 0, h = 0; }; +struct InputFieldGeometry { + double_t x = 0, y = 0; +}; + class TizenInputMethodContext { public: TizenInputMethodContext(uintptr_t window_id); @@ -47,6 +55,10 @@ class TizenInputMethodContext { bool is_down); #endif + void SetInputFieldGeometry(InputFieldGeometry); + + InputFieldGeometry GetInputFieldGeometry(); + InputPanelGeometry GetInputPanelGeometry(); void ResetInputMethodContext(); @@ -57,6 +69,10 @@ class TizenInputMethodContext { bool IsInputPanelShown(); + void SetEnableSuggestions(bool enable); + + void SetInputAction(const std::string& input_action); + void SetInputPanelLayout(const std::string& layout); void SetInputPanelLayoutVariation(bool is_signed, bool is_decimal); @@ -79,7 +95,6 @@ class TizenInputMethodContext { void RegisterEventCallbacks(); void UnregisterEventCallbacks(); - void SetContextOptions(); void SetInputPanelOptions(); #ifdef NUI_SUPPORT @@ -92,6 +107,8 @@ class TizenInputMethodContext { OnPreeditEnd on_preedit_end_; std::unordered_map event_callbacks_; + + InputFieldGeometry input_field_geometry_; }; } // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_view_elementary.cc b/flutter/shell/platform/tizen/tizen_view_elementary.cc index 037d00a..de5aade 100644 --- a/flutter/shell/platform/tizen/tizen_view_elementary.cc +++ b/flutter/shell/platform/tizen/tizen_view_elementary.cc @@ -8,8 +8,19 @@ #include #include "flutter/shell/platform/tizen/logger.h" +#ifndef WEARABLE_PROFILE +#include "flutter/shell/platform/tizen/tizen_autofill.h" +#endif #include "flutter/shell/platform/tizen/tizen_view_event_handler_delegate.h" +#if defined(MOBILE_PROFILE) +constexpr double kProfileFactor = 0.7; +#elif defined(TV_PROFILE) +constexpr double kProfileFactor = 2.0; +#elif not defined(WEARABLE_PROFILE) +constexpr double kProfileFactor = 1.0; +#endif + namespace flutter { namespace { @@ -52,6 +63,9 @@ TizenViewElementary::TizenViewElementary(int32_t width, } RegisterEventHandlers(); +#ifndef WEARABLE_PROFILE + PrepareAutofill(); +#endif PrepareInputMethod(); Show(); } @@ -94,6 +108,14 @@ bool TizenViewElementary::CreateView() { evas_object_image_size_set(image_, initial_width_, initial_height_); evas_object_image_alpha_set(image_, EINA_TRUE); elm_object_part_content_set(container_, "overlay", image_); + +#ifndef WEARABLE_PROFILE + ctxpopup_ = elm_ctxpopup_add(container_); + elm_ctxpopup_direction_priority_set( + ctxpopup_, ELM_CTXPOPUP_DIRECTION_DOWN, ELM_CTXPOPUP_DIRECTION_RIGHT, + ELM_CTXPOPUP_DIRECTION_LEFT, ELM_CTXPOPUP_DIRECTION_UP); +#endif + return true; } @@ -283,6 +305,16 @@ void TizenViewElementary::RegisterEventHandlers() { }; evas_object_smart_callback_add(container_, "focused", focused_callback_, this); + +#ifndef WEARABLE_PROFILE + evas_object_callbacks_[EVAS_CALLBACK_HIDE] = + [](void* data, Evas* e, Evas_Object* obj, void* event_info) { + elm_ctxpopup_clear(obj); + }; + evas_object_event_callback_add(ctxpopup_, EVAS_CALLBACK_HIDE, + evas_object_callbacks_[EVAS_CALLBACK_HIDE], + nullptr); +#endif } void TizenViewElementary::UnregisterEventHandlers() { @@ -306,6 +338,11 @@ void TizenViewElementary::UnregisterEventHandlers() { evas_object_event_callback_del(container_, EVAS_CALLBACK_KEY_UP, evas_object_callbacks_[EVAS_CALLBACK_KEY_UP]); evas_object_smart_callback_del(container_, "focused", focused_callback_); + +#ifndef WEARABLE_PROFILE + evas_object_event_callback_del(ctxpopup_, EVAS_CALLBACK_HIDE, + evas_object_callbacks_[EVAS_CALLBACK_HIDE]); +#endif } TizenGeometry TizenViewElementary::GetGeometry() { @@ -339,6 +376,41 @@ void TizenViewElementary::Show() { evas_object_show(image_); } +#ifndef WEARABLE_PROFILE +void TizenViewElementary::PrepareAutofill() { + TizenAutofill& autofill = TizenAutofill::GetInstance(); + autofill.SetOnPopup([this]() { + const std::vector>& items = + TizenAutofill::GetInstance().GetResponseItems(); + if (items.empty()) { + return; + } + for (const auto& item : items) { + elm_ctxpopup_item_append( + ctxpopup_, item->label.c_str(), nullptr, + [](void* data, Evas_Object* obj, void* event_info) { + AutofillItem* item = static_cast(data); + TizenAutofill::GetInstance().OnCommit(item->value); + evas_object_hide(obj); + }, + item.get()); + } +// TODO(Swanseo0): Change ctxpopup's position to focused input field. +#ifdef TV_PROFILE + double_t dpi = 72.0; +#else + double_t dpi = static_cast(GetDpi()); +#endif + double_t scale_factor = dpi / 90.0 * kProfileFactor; + InputFieldGeometry geometry = + input_method_context_->GetInputFieldGeometry(); + evas_object_move(ctxpopup_, geometry.x * scale_factor, + geometry.y * scale_factor); + evas_object_show(ctxpopup_); + }); +} +#endif + void TizenViewElementary::PrepareInputMethod() { input_method_context_ = std::make_unique(GetWindowId()); diff --git a/flutter/shell/platform/tizen/tizen_view_elementary.h b/flutter/shell/platform/tizen/tizen_view_elementary.h index 27b8aeb..7a761ab 100644 --- a/flutter/shell/platform/tizen/tizen_view_elementary.h +++ b/flutter/shell/platform/tizen/tizen_view_elementary.h @@ -46,11 +46,18 @@ class TizenViewElementary : public TizenView { void UnregisterEventHandlers(); +#ifndef WEARABLE_PROFILE + void PrepareAutofill(); +#endif + void PrepareInputMethod(); Evas_Object* parent_ = nullptr; Evas_Object* container_ = nullptr; Evas_Object* image_ = nullptr; +#ifndef WEARABLE_PROFILE + Evas_Object* ctxpopup_ = nullptr; +#endif std::unordered_map evas_object_callbacks_; diff --git a/flutter/shell/platform/tizen/tizen_view_nui.cc b/flutter/shell/platform/tizen/tizen_view_nui.cc index c9eb2eb..5215f48 100644 --- a/flutter/shell/platform/tizen/tizen_view_nui.cc +++ b/flutter/shell/platform/tizen/tizen_view_nui.cc @@ -11,6 +11,14 @@ #include "flutter/shell/platform/tizen/logger.h" #include "flutter/shell/platform/tizen/tizen_view_event_handler_delegate.h" +#if defined(MOBILE_PROFILE) +constexpr double kProfileFactor = 0.7; +#elif defined(TV_PROFILE) +constexpr double kProfileFactor = 2.0; +#elif not defined(WEARABLE_PROFILE) +constexpr double kProfileFactor = 1.0; +#endif + namespace flutter { TizenViewNui::TizenViewNui(int32_t width, @@ -23,6 +31,7 @@ TizenViewNui::TizenViewNui(int32_t width, native_image_queue_(native_image_queue), default_window_id_(default_window_id) { RegisterEventHandlers(); + PrepareAutofill(); PrepareInputMethod(); Show(); } @@ -97,6 +106,25 @@ void TizenViewNui::OnKey(const char* device_name, } } +void TizenViewNui::PrepareAutofill() { + TizenAutofill& autofill = TizenAutofill::GetInstance(); + autofill.SetOnPopup([this]() { +#ifdef TV_PROFILE + double_t dpi = 72.0; +#else + double_t dpi = static_cast(GetDpi()); +#endif + double_t scale_factor = dpi / 90.0 * kProfileFactor; + InputFieldGeometry geometry = + input_method_context_->GetInputFieldGeometry(); + autofill_.Show(image_view_, geometry.x * scale_factor, + geometry.y * scale_factor); + }); + + autofill_.SetOnCommit( + [this](std::string str) { view_delegate_->OnCommit(str); }); +} + void TizenViewNui::PrepareInputMethod() { input_method_context_ = std::make_unique(GetWindowId()); diff --git a/flutter/shell/platform/tizen/tizen_view_nui.h b/flutter/shell/platform/tizen/tizen_view_nui.h index 5ddf917..81b73be 100644 --- a/flutter/shell/platform/tizen/tizen_view_nui.h +++ b/flutter/shell/platform/tizen/tizen_view_nui.h @@ -12,6 +12,7 @@ #include +#include "flutter/shell/platform/tizen/nui_autofill_popup.h" #include "flutter/shell/platform/tizen/tizen_view.h" namespace flutter { @@ -58,6 +59,8 @@ class TizenViewNui : public TizenView { void UnregisterEventHandlers(); + void PrepareAutofill(); + void PrepareInputMethod(); void RenderOnce(); @@ -66,6 +69,7 @@ class TizenViewNui : public TizenView { Dali::NativeImageSourceQueuePtr native_image_queue_; int32_t default_window_id_; std::unique_ptr rendering_callback_; + NuiAutofillPopup autofill_; }; } // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_window_elementary.cc b/flutter/shell/platform/tizen/tizen_window_elementary.cc index ea993b8..0615ce5 100644 --- a/flutter/shell/platform/tizen/tizen_window_elementary.cc +++ b/flutter/shell/platform/tizen/tizen_window_elementary.cc @@ -8,9 +8,22 @@ #include #include +#include + #include "flutter/shell/platform/tizen/logger.h" +#ifndef WEARABLE_PROFILE +#include "flutter/shell/platform/tizen/tizen_autofill.h" +#endif #include "flutter/shell/platform/tizen/tizen_view_event_handler_delegate.h" +#if defined(MOBILE_PROFILE) +constexpr double kProfileFactor = 0.7; +#elif defined(TV_PROFILE) +constexpr double kProfileFactor = 2.0; +#elif not defined(WEARABLE_PROFILE) +constexpr double kProfileFactor = 1.0; +#endif + namespace flutter { namespace { @@ -56,6 +69,9 @@ TizenWindowElementary::TizenWindowElementary( SetWindowOptions(); RegisterEventHandlers(); +#ifndef WEARABLE_PROFILE + PrepareAutofill(); +#endif PrepareInputMethod(); Show(); } @@ -108,6 +124,13 @@ bool TizenWindowElementary::CreateWindow() { initial_geometry_.height); evas_object_raise(elm_win_); +#ifndef WEARABLE_PROFILE + ctxpopup_ = elm_ctxpopup_add(elm_win_); + elm_ctxpopup_direction_priority_set( + ctxpopup_, ELM_CTXPOPUP_DIRECTION_DOWN, ELM_CTXPOPUP_DIRECTION_RIGHT, + ELM_CTXPOPUP_DIRECTION_LEFT, ELM_CTXPOPUP_DIRECTION_UP); +#endif + image_ = evas_object_image_filled_add(evas_object_evas_get(elm_win_)); evas_object_resize(image_, initial_geometry_.width, initial_geometry_.height); evas_object_move(image_, initial_geometry_.left, initial_geometry_.top); @@ -130,6 +153,9 @@ bool TizenWindowElementary::CreateWindow() { void TizenWindowElementary::DestroyWindow() { evas_object_del(elm_win_); evas_object_del(image_); +#ifndef WEARABLE_PROFILE + evas_object_del(ctxpopup_); +#endif } void TizenWindowElementary::SetWindowOptions() { @@ -321,6 +347,16 @@ void TizenWindowElementary::RegisterEventHandlers() { evas_object_event_callback_add(elm_win_, EVAS_CALLBACK_KEY_UP, evas_object_callbacks_[EVAS_CALLBACK_KEY_UP], this); + +#ifndef WEARABLE_PROFILE + evas_object_callbacks_[EVAS_CALLBACK_HIDE] = + [](void* data, Evas* e, Evas_Object* obj, void* event_info) { + elm_ctxpopup_clear(obj); + }; + evas_object_event_callback_add(ctxpopup_, EVAS_CALLBACK_HIDE, + evas_object_callbacks_[EVAS_CALLBACK_HIDE], + nullptr); +#endif } void TizenWindowElementary::UnregisterEventHandlers() { @@ -345,6 +381,11 @@ void TizenWindowElementary::UnregisterEventHandlers() { evas_object_callbacks_[EVAS_CALLBACK_KEY_DOWN]); evas_object_event_callback_del(elm_win_, EVAS_CALLBACK_KEY_UP, evas_object_callbacks_[EVAS_CALLBACK_KEY_UP]); + +#ifndef WEARABLE_PROFILE + evas_object_event_callback_del(ctxpopup_, EVAS_CALLBACK_HIDE, + evas_object_callbacks_[EVAS_CALLBACK_HIDE]); +#endif } TizenGeometry TizenWindowElementary::GetGeometry() { @@ -411,6 +452,41 @@ void TizenWindowElementary::Show() { evas_object_show(elm_win_); } +#ifndef WEARABLE_PROFILE +void TizenWindowElementary::PrepareAutofill() { + TizenAutofill& autofill = TizenAutofill::GetInstance(); + autofill.SetOnPopup([this]() { + const std::vector>& items = + TizenAutofill::GetInstance().GetResponseItems(); + if (items.empty()) { + return; + } + for (const auto& item : items) { + elm_ctxpopup_item_append( + ctxpopup_, item->label.c_str(), nullptr, + [](void* data, Evas_Object* obj, void* event_info) { + AutofillItem* item = static_cast(data); + TizenAutofill::GetInstance().OnCommit(item->value); + evas_object_hide(obj); + }, + item.get()); + } +// TODO(Swanseo0): Change ctxpopup's position to focused input field. +#ifdef TV_PROFILE + double_t dpi = 72.0; +#else + double_t dpi = static_cast(GetDpi()); +#endif + double_t scale_factor = dpi / 90.0 * kProfileFactor; + InputFieldGeometry geometry = + input_method_context_->GetInputFieldGeometry(); + evas_object_move(ctxpopup_, geometry.x * scale_factor, + geometry.y * scale_factor); + evas_object_show(ctxpopup_); + }); +} +#endif + void TizenWindowElementary::PrepareInputMethod() { input_method_context_ = std::make_unique(GetWindowId()); diff --git a/flutter/shell/platform/tizen/tizen_window_elementary.h b/flutter/shell/platform/tizen/tizen_window_elementary.h index ea8aac7..989321c 100644 --- a/flutter/shell/platform/tizen/tizen_window_elementary.h +++ b/flutter/shell/platform/tizen/tizen_window_elementary.h @@ -68,10 +68,17 @@ class TizenWindowElementary : public TizenWindow { void UnregisterEventHandlers(); +#ifndef WEARABLE_PROFILE + void PrepareAutofill(); +#endif + void PrepareInputMethod(); Evas_Object* elm_win_ = nullptr; Evas_Object* image_ = nullptr; +#ifndef WEARABLE_PROFILE + Evas_Object* ctxpopup_ = nullptr; +#endif Evas_Smart_Cb rotation_changed_callback_ = nullptr; std::unordered_map diff --git a/tools/generate_sysroot.py b/tools/generate_sysroot.py index 9e9be17..67ae2d9 100755 --- a/tools/generate_sysroot.py +++ b/tools/generate_sysroot.py @@ -41,6 +41,10 @@ 'capi-system-info-devel', 'capi-system-system-settings', 'capi-system-system-settings-devel', + 'capi-ui-autofill', + 'capi-ui-autofill-devel', + 'capi-ui-autofill-common', + 'capi-ui-autofill-common-devel', 'capi-ui-efl-util', 'capi-ui-efl-util-devel', 'cbhm',