1/* 2 * Copyright (c) 2021 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// Nativeview 17// implemented in C++ for release 18// and in utest/view_native_mock.ts for testing 19class View extends NativeView { 20 constructor(compilerAssignedUniqueChildId, parent) { 21 super(compilerAssignedUniqueChildId, parent); 22 this.propsUsedForRender = new Set(); 23 this.isRenderingInProgress = false; 24 this.watchedProps = new Map(); 25 this.id_ = SubscriberManager.Get().MakeId(); 26 this.providedVars_ = parent ? new Map(parent.providedVars_) 27 : new Map(); 28 SubscriberManager.Get().add(this); 29 aceConsole.debug(`${this.constructor.name}: constructor done`); 30 } 31 // globally unique id, this is different from compilerAssignedUniqueChildId! 32 id() { 33 return this.id_; 34 } 35 propertyHasChanged(info) { 36 if (info) { 37 // need to sync container instanceId to switch instanceId in C++ side. 38 this.syncInstanceId(); 39 if (this.propsUsedForRender.has(info)) { 40 aceConsole.debug(`${this.constructor.name}: propertyHasChanged ['${info || "unknowm"}']. View needs update`); 41 this.markNeedUpdate(); 42 } 43 else { 44 aceConsole.debug(`${this.constructor.name}: propertyHasChanged ['${info || "unknowm"}']. View does NOT need update`); 45 } 46 let cb = this.watchedProps.get(info); 47 if (cb) { 48 aceConsole.debug(`${this.constructor.name}: propertyHasChanged ['${info || "unknowm"}']. calling @Watch function`); 49 cb.call(this, info); 50 } 51 this.restoreInstanceId(); 52 } // if info avail. 53 } 54 propertyRead(info) { 55 aceConsole.debug(`${this.constructor.name}: propertyRead ['${info || "unknowm"}'].`); 56 if (info && (info != "unknown") && this.isRenderingInProgress) { 57 this.propsUsedForRender.add(info); 58 } 59 } 60 // for test purposes 61 propertiesNeededToRender() { 62 return this.propsUsedForRender; 63 } 64 aboutToRender() { 65 aceConsole.log(`${this.constructor.name}: aboutToRender`); 66 // reset 67 this.propsUsedForRender = new Set(); 68 this.isRenderingInProgress = true; 69 } 70 aboutToContinueRender() { 71 // do not reset 72 //this.propsUsedForRender = new Set<string>(); 73 this.isRenderingInProgress = true; 74 } 75 onRenderDone() { 76 this.isRenderingInProgress = false; 77 aceConsole.log(`${this.constructor.name}: onRenderDone: render performed get access to these properties: ${JSON.stringify(Array.from(this.propsUsedForRender))}.`); 78 } 79 /** 80 * Function to be called from the constructor of the sub component 81 * to register a @Watch varibale 82 * @param propStr name of the variable. Note from @Provide and @Consume this is 83 * the variable name and not the alias! 84 * @param callback application defined member function of sub-class 85 */ 86 declareWatch(propStr, callback) { 87 this.watchedProps.set(propStr, callback); 88 } 89 /** 90 * This View @Provide's a variable under given name 91 * Call this function from the constructor of the sub class 92 * @param providedPropName either the variable name or the alias defined as 93 * decorator param 94 * @param store the backing store object for this variable (not the get/set variable!) 95 */ 96 addProvidedVar(providedPropName, store) { 97 if (this.providedVars_.has(providedPropName)) { 98 throw new ReferenceError(`${this.constructor.name}: duplicate @Provide property with name ${providedPropName}. 99 Property with this name is provided by one of the ancestor Views already.`); 100 } 101 this.providedVars_.set(providedPropName, store); 102 } 103 /** 104 * Method for the sub-class to call from its constructor for resolving 105 * a @Consume variable and initializing its backing store 106 * with the yncedPropertyTwoWay<T> object created from the 107 * @Provide variable's backing store. 108 * @param providedPropName the name of the @Provide'd variable. 109 * This is either the @Consume decortor parameter, or variable name. 110 * @param consumeVarName the @Consume variable name (not the 111 * @Consume decortor parameter) 112 * @returns initiaizing value of the @Consume backing store 113 */ 114 initializeConsume(providedPropName, consumeVarName) { 115 let providedVarStore = this.providedVars_.get(providedPropName); 116 if (providedVarStore === undefined) { 117 throw new ReferenceError(`${this.constructor.name}: missing @Provide property with name ${providedPropName}. 118 Fail to resolve @Consume(${providedPropName}).`); 119 } 120 return providedVarStore.createLink(this, consumeVarName); 121 } 122} 123 124function getContentStorage(view) { 125 return view.getContentStorage(); 126} 127 128function getContext(view) { 129 return view.getContext(); 130} 131 132class PersistentStorage { 133 constructor() { 134 this.links_ = new Map(); 135 this.id_ = SubscriberManager.Get().MakeId(); 136 SubscriberManager.Get().add(this); 137 } 138 /** 139 * 140 * @param storage method to be used by the framework to set the backend 141 * this is to be done during startup 142 */ 143 static ConfigureBackend(storage) { 144 PersistentStorage.Storage_ = storage; 145 } 146 static GetOrCreate() { 147 if (PersistentStorage.Instance_) { 148 // already initialized 149 return PersistentStorage.Instance_; 150 } 151 PersistentStorage.Instance_ = new PersistentStorage(); 152 return PersistentStorage.Instance_; 153 } 154 static AboutToBeDeleted() { 155 if (!PersistentStorage.Instance_) { 156 return; 157 } 158 PersistentStorage.GetOrCreate().aboutToBeDeleted(); 159 PersistentStorage.Instance_ = undefined; 160 } 161 static PersistProp(key, defaultValue) { 162 PersistentStorage.GetOrCreate().persistProp(key, defaultValue); 163 } 164 static DeleteProp(key) { 165 PersistentStorage.GetOrCreate().deleteProp(key); 166 } 167 static PersistProps(properties) { 168 PersistentStorage.GetOrCreate().persistProps(properties); 169 } 170 static Keys() { 171 let result = []; 172 const it = PersistentStorage.GetOrCreate().keys(); 173 let val = it.next(); 174 while (!val.done) { 175 result.push(val.value); 176 val = it.next(); 177 } 178 return result; 179 } 180 keys() { 181 return this.links_.keys(); 182 } 183 persistProp(propName, defaultValue) { 184 if (this.persistProp1(propName, defaultValue)) { 185 // persist new prop 186 aceConsole.debug(`PersistentStorage: writing '${propName}' - '${this.links_.get(propName)}' to storage`); 187 PersistentStorage.Storage_.set(propName, JSON.stringify(this.links_.get(propName).get())); 188 } 189 } 190 // helper function to persist a property 191 // does everything except writing prop to disk 192 persistProp1(propName, defaultValue) { 193 if (defaultValue == null || defaultValue == undefined) { 194 aceConsole.error(`PersistentStorage: persistProp for ${propName} called with 'null' or 'undefined' default value!`); 195 return false; 196 } 197 if (this.links_.get(propName)) { 198 aceConsole.warn(`PersistentStorage: persistProp: ${propName} is already persisted`); 199 return false; 200 } 201 let link = AppStorage.GetOrCreate().link(propName, this); 202 if (link) { 203 aceConsole.debug(`PersistentStorage: persistProp ${propName} in AppStorage, using that`); 204 this.links_.set(propName, link); 205 } 206 else { 207 let newValue = PersistentStorage.Storage_.get(propName); 208 let returnValue; 209 if (!newValue || newValue == "") { 210 aceConsole.debug(`PersistentStorage: no entry for ${propName}, will initialize with default value`); 211 returnValue = defaultValue; 212 } else { 213 try { 214 returnValue = JSON.parse(newValue); 215 } 216 catch (error) { 217 aceConsole.error(`PersistentStorage: convert for ${propName} has error: ` + error.toString()); 218 } 219 } 220 link = AppStorage.GetOrCreate().setAndLink(propName, returnValue, this); 221 this.links_.set(propName, link); 222 aceConsole.debug(`PersistentStorage: created new persistent prop for ${propName}`); 223 } 224 return true; 225 } 226 persistProps(properties) { 227 properties.forEach(property => this.persistProp1(property.key, property.defaultValue)); 228 this.write(); 229 } 230 deleteProp(propName) { 231 let link = this.links_.get(propName); 232 if (link) { 233 link.aboutToBeDeleted(); 234 this.links_.delete(propName); 235 PersistentStorage.Storage_.delete(propName); 236 aceConsole.debug(`PersistentStorage: deleteProp: no longer persisting '${propName}'.`); 237 } 238 else { 239 aceConsole.warn(`PersistentStorage: '${propName}' is not a persisted property warning.`); 240 } 241 } 242 write() { 243 this.links_.forEach((link, propName, map) => { 244 aceConsole.debug(`PersistentStorage: writing ${propName} to storage`); 245 PersistentStorage.Storage_.set(propName, JSON.stringify(link.get())); 246 }); 247 } 248 propertyHasChanged(info, isCrossWindow) { 249 aceConsole.debug("PersistentStorage: property changed"); 250 if (isCrossWindow) { 251 aceConsole.debug(`PersistentStorage propertyHasChanged isCrossWindow is ${isCrossWindow}`); 252 } else { 253 this.write(); 254 } 255 } 256 // public required by the interface, use the static method instead! 257 aboutToBeDeleted() { 258 aceConsole.debug("PersistentStorage: about to be deleted"); 259 this.links_.forEach((val, key, map) => { 260 aceConsole.debug(`PersistentStorage: removing ${key}`); 261 val.aboutToBeDeleted(); 262 }); 263 this.links_.clear(); 264 SubscriberManager.Get().delete(this.id()); 265 PersistentStorage.Storage_.clear(); 266 } 267 id() { 268 return this.id_; 269 } 270 /** 271 * This methid offers a way to force writing the property value with given 272 * key to persistent storage. 273 * In the general case this is unnecessary as the framework observed changes 274 * and triggers writing to disk by itself. For nested objects (e.g. array of 275 * objects) however changes of a property of a property as not observed. This 276 * is the case where the application needs to signal to the framework. 277 * @param key property that has changed 278 */ 279 static NotifyHasChanged(propName) { 280 aceConsole.debug(`PersistentStorage: force writing '${propName}' - '${PersistentStorage.GetOrCreate().links_.get(propName)}' to storage`); 281 PersistentStorage.Storage_.set(propName, JSON.stringify(PersistentStorage.GetOrCreate().links_.get(propName).get())); 282 } 283} 284PersistentStorage.Instance_ = undefined; 285 286class Environment { 287 constructor() { 288 this.props_ = new Map(); 289 Environment.EnvBackend_.onValueChanged(this.onValueChanged.bind(this)); 290 } 291 static GetOrCreate() { 292 if (Environment.Instance_) { 293 // already initialized 294 return Environment.Instance_; 295 } 296 Environment.Instance_ = new Environment(); 297 return Environment.Instance_; 298 } 299 static ConfigureBackend(envBackend) { 300 Environment.EnvBackend_ = envBackend; 301 } 302 static AboutToBeDeleted() { 303 if (!Environment.Instance_) { 304 return; 305 } 306 Environment.GetOrCreate().aboutToBeDeleted(); 307 Environment.Instance_ = undefined; 308 } 309 static EnvProp(key, value) { 310 return Environment.GetOrCreate().envProp(key, value); 311 } 312 static EnvProps(props) { 313 Environment.GetOrCreate().envProps(props); 314 } 315 static Keys() { 316 return Environment.GetOrCreate().keys(); 317 } 318 envProp(key, value) { 319 let prop = AppStorage.Prop(key); 320 if (prop) { 321 aceConsole.warn(`Environment: envProp '${key}': Property already exists in AppStorage. Not using environment property.`); 322 return false; 323 } 324 let tmp; 325 switch (key) { 326 case "accessibilityEnabled": 327 tmp = Environment.EnvBackend_.getAccessibilityEnabled(); 328 break; 329 case "colorMode": 330 tmp = Environment.EnvBackend_.getColorMode(); 331 break; 332 case "fontScale": 333 tmp = Environment.EnvBackend_.getFontScale().toFixed(2); 334 break; 335 case "fontWeightScale": 336 tmp = Environment.EnvBackend_.getFontWeightScale().toFixed(2); 337 break; 338 case "layoutDirection": 339 tmp = Environment.EnvBackend_.getLayoutDirection(); 340 break; 341 case "languageCode": 342 tmp = Environment.EnvBackend_.getLanguageCode(); 343 break; 344 default: 345 tmp = value; 346 } 347 prop = AppStorage.SetAndProp(key, tmp); 348 this.props_.set(key, prop); 349 aceConsole.debug(`Environment: envProp for '${key}' done.`); 350 } 351 envProps(properties) { 352 properties.forEach(property => { 353 this.envProp(property.key, property.defaultValue); 354 aceConsole.debug(`Environment: envProps for '${property.key}' done.`); 355 }); 356 } 357 keys() { 358 let result = []; 359 const it = this.props_.keys(); 360 let val = it.next(); 361 while (!val.done) { 362 result.push(val.value); 363 val = it.next(); 364 } 365 return result; 366 } 367 onValueChanged(key, value) { 368 let ok = AppStorage.Set(key, value); 369 if (ok) { 370 aceConsole.debug(`Environment: onValueChanged: ${key} changed to ${value}`); 371 } 372 else { 373 aceConsole.warn(`Environment: onValueChanged: error changing ${key}! See results above.`); 374 } 375 } 376 aboutToBeDeleted() { 377 this.props_.forEach((val, key, map) => { 378 val.aboutToBeDeleted(); 379 AppStorage.Delete(key); 380 }); 381 } 382} 383Environment.Instance_ = undefined; 384var global = globalThis; 385aceConsole.debug("ACE State Mgmt init ..."); 386PersistentStorage.ConfigureBackend(new Storage()); 387Environment.ConfigureBackend(new EnvironmentSetting()); 388 389function notifyAppStorageChange(key, value) { 390 aceConsole.debug(`notifyAppStorageChange(${key}, ${value})`); 391 if (value === "undefined") { 392 return; 393 } 394 AppStorage.GetOrCreate().crossWindowNotify(key, value); 395} 396 397class Clipboard { 398 static set(type, value) { 399 JSClipboard.set(value); 400 } 401 402 static get(type) { 403 return new Promise((resolve, reject) => { 404 const callback = () => { 405 resolve(); 406 }; 407 JSClipboard.get(callback.bind(this)); 408 }) 409 } 410 411 static clear() { 412 JSClipboard.clear(); 413 } 414}