1 /*
2 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "bridge/declarative_frontend/jsview/dialog/js_custom_dialog_controller.h"
17
18 #include "base/subwindow/subwindow_manager.h"
19 #include "bridge/declarative_frontend/engine/jsi/jsi_types.h"
20 #include "core/common/ace_engine.h"
21 #include "core/common/container.h"
22 #include "core/components_ng/base/frame_node.h"
23 #include "core/pipeline_ng/pipeline_context.h"
24 #include "frameworks/bridge/common/utils/engine_helper.h"
25 #include "frameworks/bridge/declarative_frontend/view_stack_processor.h"
26 #include "frameworks/core/components_ng/base/view_stack_processor.h"
27
28 namespace OHOS::Ace::Framework {
29 namespace {
30
31 constexpr uint32_t DELAY_TIME_FOR_STACK = 100;
32 const std::vector<DialogAlignment> DIALOG_ALIGNMENT = { DialogAlignment::TOP, DialogAlignment::CENTER,
33 DialogAlignment::BOTTOM, DialogAlignment::DEFAULT, DialogAlignment::TOP_START, DialogAlignment::TOP_END,
34 DialogAlignment::CENTER_START, DialogAlignment::CENTER_END, DialogAlignment::BOTTOM_START,
35 DialogAlignment::BOTTOM_END };
36 constexpr int32_t DEFAULT_ANIMATION_DURATION = 200;
37
38 } // namespace
39
ConstructorCallback(const JSCallbackInfo & info)40 void JSCustomDialogController::ConstructorCallback(const JSCallbackInfo& info)
41 {
42 int argc = info.Length();
43 if (argc > 1 && !info[0]->IsUndefined() && info[0]->IsObject() && !info[1]->IsUndefined() && info[1]->IsObject()) {
44 JSRef<JSObject> constructorArg = JSRef<JSObject>::Cast(info[0]);
45 JSRef<JSObject> ownerObj = JSRef<JSObject>::Cast(info[1]);
46
47 // check if owner object is set
48 JSView* ownerView = ownerObj->Unwrap<JSView>();
49 if (ownerView == nullptr) {
50 LOGE("JSCustomDialogController creation with invalid arguments. Missing \'ownerView\'");
51 return;
52 }
53
54 auto instance = new JSCustomDialogController(ownerView);
55 instance->ownerView_ = ownerView;
56
57 // Process builder function.
58 JSRef<JSVal> builderCallback = constructorArg->GetProperty("builder");
59 if (!builderCallback->IsUndefined() && builderCallback->IsFunction()) {
60 instance->jsBuilderFunction_ =
61 AceType::MakeRefPtr<JsFunction>(ownerObj, JSRef<JSFunc>::Cast(builderCallback));
62 } else {
63 delete instance;
64 instance = nullptr;
65 LOGE("JSCustomDialogController invalid builder function argument");
66 return;
67 }
68
69 // Process cancel function.
70 JSRef<JSVal> cancelCallback = constructorArg->GetProperty("cancel");
71 if (!cancelCallback->IsUndefined() && cancelCallback->IsFunction()) {
72 auto jsCancelFunction = AceType::MakeRefPtr<JsFunction>(ownerObj, JSRef<JSFunc>::Cast(cancelCallback));
73 instance->jsCancelFunction_ = jsCancelFunction;
74
75 if (Container::IsCurrentUseNewPipeline()) {
76 auto onCancel = [execCtx = info.GetExecutionContext(), func = std::move(jsCancelFunction)]() {
77 JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx);
78 ACE_SCORING_EVENT("onCancel");
79 func->Execute();
80 };
81 instance->dialogProperties_.onCancel = onCancel;
82 }
83 }
84
85 // Parses autoCancel.
86 JSRef<JSVal> autoCancelValue = constructorArg->GetProperty("autoCancel");
87 if (autoCancelValue->IsBoolean()) {
88 instance->dialogProperties_.autoCancel = autoCancelValue->ToBoolean();
89 }
90
91 // Parses customStyle.
92 JSRef<JSVal> customStyleValue = constructorArg->GetProperty("customStyle");
93 if (customStyleValue->IsBoolean()) {
94 instance->dialogProperties_.customStyle = customStyleValue->ToBoolean();
95 }
96
97 // Parse alignment
98 auto alignmentValue = constructorArg->GetProperty("alignment");
99 if (alignmentValue->IsNumber()) {
100 auto alignment = alignmentValue->ToNumber<int32_t>();
101 if (alignment >= 0 && alignment <= static_cast<int32_t>(DIALOG_ALIGNMENT.size())) {
102 instance->dialogProperties_.alignment = DIALOG_ALIGNMENT[alignment];
103 }
104 }
105
106 // Parse offset
107 auto offsetValue = constructorArg->GetProperty("offset");
108 if (offsetValue->IsObject()) {
109 auto offsetObj = JSRef<JSObject>::Cast(offsetValue);
110 Dimension dx;
111 auto dxValue = offsetObj->GetProperty("dx");
112 JSViewAbstract::ParseJsDimensionVp(dxValue, dx);
113 Dimension dy;
114 auto dyValue = offsetObj->GetProperty("dy");
115 JSViewAbstract::ParseJsDimensionVp(dyValue, dy);
116 dx.ResetInvalidValue();
117 dy.ResetInvalidValue();
118 instance->dialogProperties_.offset = DimensionOffset(dx, dy);
119 }
120
121 // Parses gridCount.
122 auto gridCountValue = constructorArg->GetProperty("gridCount");
123 if (gridCountValue->IsNumber()) {
124 instance->dialogProperties_.gridCount = gridCountValue->ToNumber<int32_t>();
125 }
126
127 // Parse maskColor.
128 auto maskColorValue = constructorArg->GetProperty("maskColor");
129 Color maskColor;
130 if (JSViewAbstract::ParseJsColor(maskColorValue, maskColor)) {
131 instance->dialogProperties_.maskColor = maskColor;
132 }
133
134 auto execContext = info.GetExecutionContext();
135 // Parse openAnimation.
136 auto openAnimationValue = constructorArg->GetProperty("openAnimation");
137 AnimationOption openAnimation;
138 if (ParseAnimation(execContext, openAnimationValue, openAnimation)) {
139 instance->dialogProperties_.openAnimation = openAnimation;
140 }
141
142 // Parse closeAnimation.
143 auto closeAnimationValue = constructorArg->GetProperty("closeAnimation");
144 AnimationOption closeAnimation;
145 if (ParseAnimation(execContext, closeAnimationValue, closeAnimation)) {
146 instance->dialogProperties_.closeAnimation = closeAnimation;
147 }
148
149 auto showInSubWindowValue = constructorArg->GetProperty("showInSubWindow");
150 if (showInSubWindowValue->IsBoolean()) {
151 #if defined(PREVIEW)
152 LOGW("[Engine Log] Unable to use the SubWindow in the Previewer. Perform this operation on the "
153 "emulator or a real device instead.");
154 #else
155 instance->dialogProperties_.isShowInSubWindow = showInSubWindowValue->ToBoolean();
156 #endif
157 }
158
159 info.SetReturnValue(instance);
160 } else {
161 LOGE("JSView creation with invalid arguments.");
162 }
163 }
164
DestructorCallback(JSCustomDialogController * controller)165 void JSCustomDialogController::DestructorCallback(JSCustomDialogController* controller)
166 {
167 if (controller != nullptr) {
168 controller->ownerView_ = nullptr;
169 delete controller;
170 }
171 }
172
NotifyDialogOperation(DialogOperation operation)173 void JSCustomDialogController::NotifyDialogOperation(DialogOperation operation)
174 {
175 LOGI("JSCustomDialogController(NotifyDialogOperation) operation: %{public}d", operation);
176 if (operation == DialogOperation::DIALOG_OPEN) {
177 isShown_ = true;
178 pending_ = false;
179 for (auto iter = dialogOperation_.begin(); iter != dialogOperation_.end();) {
180 if (*iter == DialogOperation::DIALOG_OPEN) {
181 dialogOperation_.erase(iter++);
182 continue;
183 }
184
185 if (*iter == DialogOperation::DIALOG_CLOSE) {
186 dialogOperation_.erase(iter);
187 CloseDialog();
188 break;
189 }
190 }
191 } else if (operation == DialogOperation::DIALOG_CLOSE) {
192 isShown_ = false;
193 pending_ = false;
194 for (auto iter = dialogOperation_.begin(); iter != dialogOperation_.end();) {
195 if (*iter == DialogOperation::DIALOG_CLOSE) {
196 dialogOperation_.erase(iter++);
197 continue;
198 }
199
200 if (*iter == DialogOperation::DIALOG_OPEN) {
201 dialogOperation_.erase(iter);
202 ShowDialog();
203 break;
204 }
205 }
206 }
207 }
208
ShowDialog()209 void JSCustomDialogController::ShowDialog()
210 {
211 LOGI("JSCustomDialogController(ShowDialog)");
212 RefPtr<Container> container;
213 auto current = Container::Current();
214 if (!current) {
215 LOGE("Container is null.");
216 return;
217 }
218 if (current->IsSubContainer()) {
219 auto parentContainerId = SubwindowManager::GetInstance()->GetParentContainerId(Container::CurrentId());
220 container = AceEngine::Get().GetContainer(parentContainerId);
221 } else {
222 container = std::move(current);
223 }
224 if (!container) {
225 LOGE("Container is null.");
226 return;
227 }
228 auto context = AceType::DynamicCast<PipelineContext>(container->GetPipelineContext());
229 if (!context) {
230 LOGE("JSCustomDialogController No Context");
231 return;
232 }
233 dialogProperties_.customComponent = customDialog_;
234 EventMarker cancelMarker([cancelCallback = jsCancelFunction_]() {
235 if (cancelCallback) {
236 ACE_SCORING_EVENT("CustomDialog.cancel");
237 cancelCallback->Execute();
238 }
239 });
240 dialogProperties_.callbacks.try_emplace("cancel", cancelMarker);
241 dialogProperties_.onStatusChanged = [this](bool isShown) {
242 if (!isShown) {
243 this->isShown_ = isShown;
244 }
245 };
246
247 auto executor = context->GetTaskExecutor();
248 if (!executor) {
249 LOGE("JSCustomDialogController(ShowDialog) No Executor. Cannot post task.");
250 return;
251 }
252
253 if (pending_) {
254 LOGI("JSCustomDialogController(ShowDialog) current state is pending.");
255 dialogOperation_.emplace_back(DialogOperation::DIALOG_OPEN);
256 return;
257 }
258
259 if (isShown_) {
260 LOGI("JSCustomDialogController(ShowDialog) CustomDialog has already shown.");
261 return;
262 }
263
264 pending_ = true;
265 auto task = [context, dialogProperties = dialogProperties_, this]() mutable {
266 if (context) {
267 this->dialogComponent_ = context->ShowDialog(dialogProperties, false, "CustomDialog");
268 } else {
269 LOGE("JSCustomDialogController(ShowDialog) context is null.");
270 }
271 this->NotifyDialogOperation(DialogOperation::DIALOG_OPEN);
272 };
273 auto stack = context->GetLastStack();
274 auto result = false;
275 if (stack) {
276 result = executor->PostTask(task, TaskExecutor::TaskType::UI);
277 } else {
278 LOGE("JSCustomDialogController(ShowDialog) stack is null, post delay task.");
279 result = executor->PostDelayedTask(task, TaskExecutor::TaskType::UI, DELAY_TIME_FOR_STACK);
280 }
281 if (!result) {
282 LOGW("JSCustomDialogController(ShowDialog) fail to post task, reset pending status");
283 pending_ = false;
284 }
285 }
286
CloseDialog()287 void JSCustomDialogController::CloseDialog()
288 {
289 LOGI("JSCustomDialogController(CloseDialog)");
290 RefPtr<Container> container;
291 auto current = Container::Current();
292 if (!current) {
293 LOGE("Container is null.");
294 return;
295 }
296 if (current->IsSubContainer()) {
297 auto parentContainerId = SubwindowManager::GetInstance()->GetParentContainerId(Container::CurrentId());
298 container = AceEngine::Get().GetContainer(parentContainerId);
299 } else {
300 container = std::move(current);
301 }
302 if (!container) {
303 LOGE("Container is null.");
304 return;
305 }
306 auto context = AceType::DynamicCast<PipelineContext>(container->GetPipelineContext());
307 if (!context) {
308 LOGE("JSCustomDialogController No Context");
309 return;
310 }
311 const auto& lastStack = context->GetLastStack();
312 if (!lastStack) {
313 LOGE("JSCustomDialogController No Stack!");
314 return;
315 }
316 auto executor = context->GetTaskExecutor();
317 if (!executor) {
318 LOGE("JSCustomDialogController(CloseDialog) No Executor. Cannot post task.");
319 return;
320 }
321
322 if (pending_) {
323 LOGI("JSCustomDialogController(CloseDialog) current state is pending.");
324 dialogOperation_.emplace_back(DialogOperation::DIALOG_CLOSE);
325 return;
326 }
327
328 pending_ = true;
329 auto task = [lastStack, dialogComponent = dialogComponent_, this]() {
330 if (!lastStack || !dialogComponent) {
331 LOGI("JSCustomDialogController(CloseDialog) stack or dialog is null.");
332 this->NotifyDialogOperation(DialogOperation::DIALOG_CLOSE);
333 return;
334 }
335 auto animator = dialogComponent->GetAnimator();
336 auto dialogId = dialogComponent->GetDialogId();
337 if (animator) {
338 if (!dialogComponent->HasStopListenerAdded()) {
339 animator->AddStopListener([lastStack, dialogId] {
340 if (lastStack) {
341 lastStack->PopDialog(dialogId);
342 }
343 });
344 dialogComponent->SetHasStopListenerAdded(true);
345 }
346 animator->Play();
347 } else {
348 lastStack->PopDialog(dialogId);
349 }
350 this->NotifyDialogOperation(DialogOperation::DIALOG_CLOSE);
351 };
352 auto result = executor->PostTask(task, TaskExecutor::TaskType::UI);
353 if (!result) {
354 LOGW("JSCustomDialogController(CloseDialog) fail to post task, reset pending status");
355 pending_ = false;
356 }
357
358 dialogComponent_ = nullptr;
359 }
360
JsOpenDialog(const JSCallbackInfo & info)361 void JSCustomDialogController::JsOpenDialog(const JSCallbackInfo& info)
362 {
363 LOGI("JSCustomDialogController(JsOpenDialog)");
364 if (!jsBuilderFunction_) {
365 LOGE("Builder of CustomDialog is null.");
366 return;
367 }
368 auto scopedDelegate = EngineHelper::GetCurrentDelegate();
369 if (!scopedDelegate) {
370 // this case usually means there is no foreground container, need to figure out the reason.
371 LOGE("scopedDelegate is null, please check");
372 return;
373 }
374
375 // NG
376 if (Container::IsCurrentUseNewPipeline()) {
377 auto container = Container::Current();
378 auto currentId = Container::CurrentId();
379 CHECK_NULL_VOID(container);
380 if (container->IsSubContainer()) {
381 currentId = SubwindowManager::GetInstance()->GetParentContainerId(Container::CurrentId());
382 container = AceEngine::Get().GetContainer(currentId);
383 }
384 ContainerScope scope(currentId);
385 auto pipelineContext = container->GetPipelineContext();
386 CHECK_NULL_VOID(pipelineContext);
387 auto context = AceType::DynamicCast<NG::PipelineContext>(pipelineContext);
388 CHECK_NULL_VOID(context);
389 auto overlayManager = context->GetOverlayManager();
390 CHECK_NULL_VOID(overlayManager);
391
392 NG::ScopedViewStackProcessor builderViewStackProcessor;
393 LOGD("custom JS node building");
394 jsBuilderFunction_->Execute();
395 auto customNode = NG::ViewStackProcessor::GetInstance()->Finish();
396 CHECK_NULL_VOID(customNode);
397
398 dialogProperties_.onStatusChanged = [this](bool isShown) {
399 if (!isShown) {
400 this->isShown_ = isShown;
401 }
402 };
403
404 WeakPtr<NG::FrameNode> dialog;
405 if (dialogProperties_.isShowInSubWindow) {
406 dialog = SubwindowManager::GetInstance()->ShowDialogNG(dialogProperties_, customNode);
407 } else {
408 dialog = overlayManager->ShowDialog(dialogProperties_, customNode, false);
409 }
410 dialogs_.emplace_back(dialog);
411 return;
412 }
413
414 // Cannot reuse component because might depend on state
415 if (customDialog_) {
416 customDialog_ = nullptr;
417 }
418
419 {
420 ACE_SCORING_EVENT("CustomDialog.builder");
421 jsBuilderFunction_->Execute();
422 }
423 customDialog_ = ViewStackProcessor::GetInstance()->Finish();
424
425 if (!customDialog_) {
426 LOGE("Builder does not generate view.");
427 return;
428 }
429
430 ShowDialog();
431 }
432
JsCloseDialog(const JSCallbackInfo & info)433 void JSCustomDialogController::JsCloseDialog(const JSCallbackInfo& info)
434 {
435 LOGI("JSCustomDialogController(JsCloseDialog)");
436 auto scopedDelegate = EngineHelper::GetCurrentDelegate();
437 if (!scopedDelegate) {
438 // this case usually means there is no foreground container, need to figure out the reason.
439 LOGE("scopedDelegate is null, please check");
440 return;
441 }
442
443 if (Container::IsCurrentUseNewPipeline()) {
444 RefPtr<NG::FrameNode> dialog;
445 while (!dialogs_.empty()) {
446 dialog = dialogs_.back().Upgrade();
447 if (dialog) {
448 // get the dialog not removed currently
449 break;
450 }
451 dialogs_.pop_back();
452 }
453
454 if (dialogs_.empty()) {
455 LOGW("dialogs are empty");
456 return;
457 }
458 if (!dialog) {
459 LOGW("dialog is null");
460 return;
461 }
462 auto container = Container::Current();
463 auto currentId = Container::CurrentId();
464 CHECK_NULL_VOID(container);
465 if (container->IsSubContainer() && !dialogProperties_.isShowInSubWindow) {
466 currentId = SubwindowManager::GetInstance()->GetParentContainerId(Container::CurrentId());
467 container = AceEngine::Get().GetContainer(currentId);
468 }
469 ContainerScope scope(currentId);
470 auto pipelineContext = container->GetPipelineContext();
471 CHECK_NULL_VOID(pipelineContext);
472 auto context = AceType::DynamicCast<NG::PipelineContext>(pipelineContext);
473 CHECK_NULL_VOID(context);
474 auto overlayManager = context->GetOverlayManager();
475 CHECK_NULL_VOID(overlayManager);
476 overlayManager->CloseDialog(dialog);
477 dialogs_.pop_back();
478 return;
479 }
480 CloseDialog();
481 }
482
ParseAnimation(const JsiExecutionContext & execContext,const JsiRef<JsiValue> & animationValue,AnimationOption & result)483 bool JSCustomDialogController::ParseAnimation(
484 const JsiExecutionContext& execContext, const JsiRef<JsiValue>& animationValue, AnimationOption& result)
485 {
486 if (animationValue->IsNull() || !animationValue->IsObject()) {
487 return false;
488 }
489 auto animationArgs = JsonUtil::ParseJsonString(animationValue->ToString());
490 if (animationArgs->IsNull()) {
491 return false;
492 }
493 // If the attribute does not exist, the default value is used.
494 auto duration = animationArgs->GetInt("duration", DEFAULT_ANIMATION_DURATION);
495 auto delay = animationArgs->GetInt("delay", 0);
496 auto iterations = animationArgs->GetInt("iterations", 1);
497 auto tempo = static_cast<float>(animationArgs->GetDouble("tempo", 1.0));
498 auto direction = StringToAnimationDirection(animationArgs->GetString("playMode", "normal"));
499 RefPtr<Curve> curve;
500 auto curveArgs = animationArgs->GetValue("curve");
501 if (curveArgs->IsString()) {
502 curve = CreateCurve(animationArgs->GetString("curve", "linear"));
503 } else if (curveArgs->IsObject()) {
504 auto curveString = curveArgs->GetValue("__curveString");
505 if (!curveString) {
506 // Default AnimationOption which is invalid.
507 return false;
508 }
509 curve = CreateCurve(curveString->GetString());
510 } else {
511 curve = Curves::EASE_IN_OUT;
512 }
513 result.SetDuration(duration);
514 result.SetDelay(delay);
515 result.SetIteration(iterations);
516 result.SetTempo(tempo);
517 result.SetAnimationDirection(direction);
518 result.SetCurve(curve);
519
520 JSRef<JSObject> obj = JSRef<JSObject>::Cast(animationValue);
521 JSRef<JSVal> onFinish = obj->GetProperty("onFinish");
522 std::function<void()> onFinishEvent;
523 if (onFinish->IsFunction()) {
524 RefPtr<JsFunction> jsFunc = AceType::MakeRefPtr<JsFunction>(JSRef<JSObject>(), JSRef<JSFunc>::Cast(onFinish));
525 onFinishEvent = [execCtx = execContext, func = std::move(jsFunc)]() {
526 JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx);
527 ACE_SCORING_EVENT("CustomDialog.onFinish");
528 func->Execute();
529 };
530 result.SetOnFinishEvent(onFinishEvent);
531 }
532 return true;
533 }
534
JSBind(BindingTarget object)535 void JSCustomDialogController::JSBind(BindingTarget object)
536 {
537 JSClass<JSCustomDialogController>::Declare("CustomDialogController");
538 JSClass<JSCustomDialogController>::CustomMethod("open", &JSCustomDialogController::JsOpenDialog);
539 JSClass<JSCustomDialogController>::CustomMethod("close", &JSCustomDialogController::JsCloseDialog);
540 JSClass<JSCustomDialogController>::Bind(
541 object, &JSCustomDialogController::ConstructorCallback, &JSCustomDialogController::DestructorCallback);
542 }
543
544 } // namespace OHOS::Ace::Framework
545