diff --git a/apps/toolbox/src/main-page.ts b/apps/toolbox/src/main-page.ts index da045f370e..4c04eb38a8 100644 --- a/apps/toolbox/src/main-page.ts +++ b/apps/toolbox/src/main-page.ts @@ -41,7 +41,7 @@ function setupSceneEvents() { // Listen to scene events Application.on(SceneEvents.sceneWillConnect, (args: SceneEventData) => { console.log('New scene connecting:', args.scene); - console.log('Window:', args.window); + console.log('Window:', args.uiWindow); console.log('Connection options:', args.connectionOptions); }); diff --git a/apps/toolbox/src/pages/multiple-scenes.ts b/apps/toolbox/src/pages/multiple-scenes.ts index 916c6449b4..f8ccac2353 100644 --- a/apps/toolbox/src/pages/multiple-scenes.ts +++ b/apps/toolbox/src/pages/multiple-scenes.ts @@ -1,523 +1,406 @@ -import { Observable, EventData, Page, Application, Frame, StackLayout, Label, Button, Dialogs, View, Color, SceneEvents, SceneEventData, Utils } from '@nativescript/core'; +import { Application, Button, Color, Dialogs, EventData, isAndroid, isIOS, Label, NativeWindowEvents, Observable, Page, StackLayout, WindowEvents } from '@nativescript/core'; +import type { NativeWindow, NativeWindowEventData, PrimaryWindowChangedEventData, WindowCloseEventData, WindowContentRequest, WindowOpenEventData } from '@nativescript/core'; -let page: Page; let viewModel: MultipleScenesModel; export function navigatingTo(args: EventData) { - page = args.object; + installWindowContentResolver(); viewModel = new MultipleScenesModel(); - page.bindingContext = viewModel; + (args.object).bindingContext = viewModel; } -export class MultipleScenesModel extends Observable { - private _sceneCount = 0; - private _isMultiSceneSupported = false; - private _currentScenes: any[] = []; - private _currentWindows: any[] = []; - private _sceneEvents: string[] = []; +export function navigatingFrom(args: EventData) { + viewModel?.destroy(); + viewModel = undefined; +} - constructor() { - super(); - this.checkSceneSupport(); - this.setupSceneEventListeners(); - this.updateSceneInfo(); - this.checkSceneDelegateRegistration(); - } +/** + * The demo can open two different kinds of window. The kind is chosen when the window is + * requested and travels to the new window as `openWindow({ data })`. + */ +type DemoWindowKind = 'newSceneBasic' | 'newSceneAlt'; - get sceneCount(): number { - return this._sceneCount; - } +const DEMO_WINDOW_KINDS: Record = { + newSceneBasic: { title: 'Basic demo window', background: '#cdffdb', accent: '#ff4444', titleColor: '#1c4d2e' }, + newSceneAlt: { title: 'Alternate demo window', background: '#65adf1', accent: '#006ead', titleColor: '#00305c' }, +}; - get isMultiSceneSupported(): boolean { - return this._isMultiSceneSupported; - } +/** Per-window events the demo mirrors into its log and its live window list. */ +const TRACKED_WINDOW_EVENTS: string[] = [NativeWindowEvents.attached, NativeWindowEvents.detached, NativeWindowEvents.activate, NativeWindowEvents.deactivate, NativeWindowEvents.background, NativeWindowEvents.foreground, NativeWindowEvents.contentLoaded, NativeWindowEvents.displayed, NativeWindowEvents.close, NativeWindowEvents.orientationChanged, NativeWindowEvents.systemAppearanceChanged, NativeWindowEvents.layoutDirectionChanged]; - get currentScenes(): any[] { - return this._currentScenes; - } +// --- Window content --- - get currentWindows(): any[] { - return this._currentWindows; - } +let contentResolverInstalled = false; - get sceneEvents(): string[] { - return this._sceneEvents; +function installWindowContentResolver() { + if (contentResolverInstalled) { + return; } + contentResolverInstalled = true; + + // The resolver stays installed for the rest of the process: a window that detaches and + // re-attaches (iOS scene reconnect, Android activity recreation) asks for its content + // again, long after this page may have been navigated away from. + Application.setWindowContentResolver(resolveWindowContent); +} - get canCreateNewScene(): boolean { - return this._isMultiSceneSupported && __APPLE__; +function resolveWindowContent(request: WindowContentRequest): Page | undefined { + // `data` is exactly what openWindow({ data }) was called with, carried across by the + // platform: NSUserActivity.userInfo on iOS, intent extras on Android. + const kind = request.data?.kind as DemoWindowKind; + + // Returning undefined hands the window back to the default main-entry behaviour, which + // is what every window this demo did not open should get - the primary window, a cold + // start, or a window the system restored on its own. + if (request.isPrimary || !DEMO_WINDOW_KINDS[kind]) { + return undefined; } - get statusText(): string { - if (!__APPLE__) { - return 'Scene support is only available on iOS'; - } - if (!this._isMultiSceneSupported) { - return 'Multi-scene support not enabled. Add scene configuration to Info.plist'; - } + viewModel?.logEvent(`content resolved for ${request.window.id} (${kind})`); - // Check which API is available - let apiInfo = ''; - try { - if (typeof UIApplication !== 'undefined') { - const app = UIApplication.sharedApplication; - if (typeof app.activateSceneSessionForRequestErrorHandler === 'function') { - apiInfo = ' (iOS 17+ API available)'; - } else if (typeof app.requestSceneSessionActivationUserActivityOptionsErrorHandler === 'function') { - apiInfo = ' (iOS 13-16 API available)'; - } - } - } catch (e) { - // Ignore errors in API detection - } + return createDemoWindowPage(kind, request.window); +} - return `Multi-scene support enabled. ${this._sceneCount} scene(s) active${apiInfo}`; +/** + * Views built in plain code are only reachable through the closures that created them, so + * an unreferenced button can be collected while its window is still on screen and stop + * responding to taps. Holding the buttons here keeps them alive for the window's lifetime. + */ +const liveCloseButtons = new Set