package votorola.s.gwt.stage; // Copyright 2012-2013, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. import com.google.gwt.core.client.*; import com.google.gwt.dom.client.*; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HasHandlers; import com.google.gwt.storage.client.Storage; import com.google.gwt.user.client.*; import com.google.web.bindery.event.shared.EventBus; import java.util.*; import votorola.a.diff.*; import votorola.a.web.gwt.*; import votorola.g.lang.*; import votorola.g.net.*; import votorola.g.web.gwt.*; import votorola.g.web.gwt.event.*; import votorola.s.gwt.stage.light.LightBank; /** The Crossforum Theatre * stage. The purpose of the stage is to support * the decoration of web pages (scenes) with specialized views and controls (props) for * the purpose of broad-based system guidance. Any web page may be "staged" in this * manner. The props are typically deployed in containers known as "{@linkplain Track * tracks}" that are layed out along the edges of the browser viewport. See {@linkplain * StageV StageV} for layout examples. The stage itself provides support to the props in * the form of common variables that model the core state of the application. This * enables the props to function in a coordinated manner while remaining independent of * each other, which in turn frees the user to make a personal choice of props based on * need or preference. Although such personal customization is not yet supported by * current implementations of the stage, it remains a design requirement for the future. * *

Persistence of state in a single page

* *

The {@linkplain TheatrePage staged state} of each page is persisted to ensure that * changes by the user (selections and so forth) are not lost due to page reload during * back and forth navigation, refresh, and so forth. (A memory cache such as Firefox's * BFCache, or WebKit's page cache will also do this, but is not available for all * browsers, nor enabled for all pages.) For this to work correctly, scenes and props * must initialize on page load vis-a-vis the stage only via {@linkplain * TheatreInitializer theatre initializers} that are {@linkplain * #addInitializer(TheatreInitializer) registered with the stage}.

* *

To clear the persisted state and load a pristine page either (A) close and reopen * the window or tab, or otherwise restart the browsing session; or (B) clear the web * store (also known as DOM or HTML5 storage) by whatever means the browser provides * (e.g. entering sessionStorage.clear() in the Firebug console); or (C) use * the following procedure to clear the state for a single page: (1) ensure the URL has a * fragment '#' delimiter by appending it if necessary; (2) also append "voStage.clear" * to the end of the URL; (3) press return as if to navigate to that fragment; and * finally (4) reload the page. For example here's a URL in its original form and with * the necessary appendage:

   http://somewhere.com/fu#bar
  *   http://somewhere.com/fu#bar&voStage.clear
* *

Persistence of state across pages

* *

The stage provides a mechanism for the selective transfer of state during transit * from one page (referrer) to another via hypertext links. This allows user selections * and such to persist across pages. This persistence is controlled by the props that * depend upon it via the {@linkplain TheatreInitializer#initTo(Stage,TheatrePage) * initTo}(stage,referrer) methods of their registered initializers.

* *

State changes during ordinary use

* *

Subsequent state changes such as those initiated by the user are generally expected * as late as the "finally" phase of each event dispatch. The associated change events * are then dispatched in the "deferred" phase where they appear atomic regardless of the * number of state variables involved. There is a minor exception to this rule: late * registration of an initializer (i) will nevertheless flush pending events immediately, * as stipulated by i.{@linkplain TheatreInitializer#initFromComplete(Stage,boolean) * initFromComplete} and {@linkplain TheatreInitializer#initToComplete(Stage,boolean) * initToComplete}.

* *

Events are propagated via the {@linkplain GWTX#bus() common event bus}. Each event * is pre-dispatched from the suppressable {@linkplain #prompter() prompter} first. The * event is cancelled if it gets suppressed, otherwise it is re-dispatched from the * ordinary source.

* *

Defaulting properties and masked changes

* *

A defaulting property is one that has a default mechanism, such as "{@linkplain * #getDifference() difference}". A defaulting property that changes from null to the * default value, or from the default value to null, exhibits no effective change in its * value. The value remains at the default and the change is said to be masked. No * change event "NAME" is fired in this case, but rather a change event "NAME * masked".

*/ public final class Stage implements HasHandlers, TheatrePage, WarningSink { /** Creates the {@linkplain #i() single instance} of Stage. */ Stage() { tracks.add( new votorola.s.gwt.stage.link.LinkTrack() ); tracks.add( new votorola.s.gwt.stage.poll.Polltrack() ); // tracks.add( new votorola.s.gwt.stage.talk.TalkTrack() ); /// pending CWFIX, http://mail.zelea.com/list/votorola/2013-June/001763.html tracks.add( new votorola.s.gwt.stage.vote.VoteTrack() ); } /** The single instance of Stage. */ public static Stage i() { return instance; } private static Stage instance; { if( instance != null ) throw new IllegalStateException(); instance = Stage.this; } private Init init = new Init(); // nulled after init.isUltimatelyComplete() private InitStep initStep = new InitPending(); // no change of stage state expected till step transits from pending /** Begins initialization of the stage. */ void init0() { new ReferrerRelayer().resolveReferrer( Stage.this ); } // called by StageMod.execute /** Continues initialization of the stage. * * @param _isReferencePending whether the referrer (if any) remains to be * resolved asynchronously. When true, _referrer must be null. */ void init1( TheatrePageJS _referrer, ReferrerRelayer _referrerRelayer, boolean _isReferencePending ) { referrer = _referrer; init.setReference( _referrerRelayer, _isReferencePending ); Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand() { // Deferral lays a trap for informal initializers that execute out of turn and // thereby risk the integrity of state restoration. It has no other purpose. public void execute() { init2a_async(); } }); } private void init2a_async() // unknown order vs. init2b_async { final Storage store = Storage.getSessionStorageIfSupported(); // The session store is open to this script even if its host or port of origin // differs from that of the page (Firefox 9, Chrome 18). Apparently it is not // subject to local storage's "same origin" restrictions of "scheme/host/port // tuple" [1], or IndexedDB's equivalent "tuple (scheme, host, port)" [2]. // // [1] http://www.w3.org/TR/webstorage/#security-localStorage // [2] http://www.w3.org/TR/html5/origin-0.html#origin-0 // as referenced from http://www.w3.org/TR/IndexedDB/ if( store != null ) { final String storageKey = getClass().getName() + " state " + pageLocation; final Storer storer = new Storer( store, storageKey ); if( referrer == null ) { final String loc = Document.get().getURL(); final boolean hasFragment = loc.length() > pageLocation.length(); if( !hasFragment || loc.endsWith("voStage.clear") ) { final String pageStateJSON = store.getItem( storageKey ); if( pageStateJSON != null ) { new Restorer(storer).init( pageStateJSON ); return; } } } } new ScratchInitializer().init(); } /** Continues initialization of the stage in the case of a deferred referrer * resolution. */ void init2b_async( final TheatrePageJS latelyResolvedReferrer ) // unknown order vs. init2a_async { Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand() { // Scheduled otherwise init2b_async can somehow interpose itself between // init2a_async - init3. That should be impossible, but printouts in devmode // show the iterposition (GWT 2.4.0+mca.1, Firefox 9) and it explains a bug // simultaneously observed (resolved referrer is dropped). public void execute() { assert init.isReferencePending; init.isReferencePending = false; referrer = latelyResolvedReferrer; if( !init.isImmediatelyComplete ) return; // init2a_async still pending, let it do the work init2bJS( latelyResolvedReferrer ); for( TheatreInitializer i: init.theatreInitializers ) { i.initUltimately( Stage.this, latelyResolvedReferrer ); } if( init.isUltimatelyComplete() ) init = null; // actually, no if about it } }); } private native void init2bJS( TheatrePageJS referrer ) /*-{ var f; try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initUltimately; } catch( e ) {} // in case parents undefined if( f ) f( referrer ); }-*/; private void init3() { init.referrerRelayer.activate(); init.isImmediatelyComplete = true; if( init.isUltimatelyComplete() ) init = null; } // ` e a r l y ```````````````````````````````````````````````````````````````````````` /** The collection of all state chunkers. */ private final LinkedList stateChunkers = new LinkedList(); // ------------------------------------------------------------------------------------ /** Adds a theatre initializer to be run either immediately or later, depending on the * current state of initialization. Initializers are automatically removed after * they are run. */ public void addInitializer( final TheatreInitializer i ) { initStep.add( i ); } /** Appends a string that encodes the state of this stage as a sequence of JSON * name/value pairs suitable for underwriting an instance of {@linkplain * TheatrePageJS TheatrePageJS}, all but for the pageLocation and the surrounding * curly braces. The value is effectively bound via the {@linkplain GWTX#bus() event * bus} to all named properties "NAME" and "NAME masked" of the stage. An event of * either form signals a probable change to the JSON value, subject only to the * possibility that multiple changes might cancel each other by the time the events * are fired. * * @see #wrapJSON(StringBuilder) * @see Defaulting properties and masked changes */ void appendJSON( final StringBuilder b ) { if( appendJSON_cache == null ) { final int c = b.length(); for( StateChunker chunker: stateChunkers ) chunker.appendJSON( b ); appendJSON_cache = b.substring( c ); } else b.append( appendJSON_cache ); } private String appendJSON_cache; // cleared eagerly in gun /** Answers whether the page is a position draft to be decorated by difference * shadows, once the necessary differences are fetched from the server. The value * may only transit from false to true and is bound via the {@linkplain GWTX#bus() * event bus} to property name differencesShadowed. When true, the style * class voDifferencesShadowed is set on the page body. * * @see #setDifferencesShadowed() * @see votorola.s.gwt.mediawiki.DifferenceShadows * @see Category:Draft */ public boolean areDifferencesShadowed() { return areDifferencesShadowed; }; private boolean areDifferencesShadowed; /** Specifies that the page is a position draft to be decorated by difference * shadows. * * @see #areDifferencesShadowed() */ public void setDifferencesShadowed() { if( areDifferencesShadowed ) return; areDifferencesShadowed = true; gun.schedule( new PropertyChange( "differencesShadowed" )); gun.phaser().schedule( gun.baseScheduler(), new Scheduler.ScheduledCommand() { public void execute() { // in the same phase with event dispatch for smoother rendering in // case a listener makes disruptive style changes of its own Document.get().getBody().addClassName( "voDifferencesShadowed" ); } }); } /** The light bank for this stage. */ public LightBank lightBank() { return lightBank; } private final LightBank lightBank = new LightBank(); /** The source from which stage events are pre-dispatched. Most handlers will name * the stage itself as the event source. Only those handlers that suppress events * should name the prompter. * * @see GWTX#bus() */ public SuppressableSource prompter() { return prompter; } private final SuppressableSource prompter = new SuppressableSource(); /** The list of all tracks on stage. */ public List tracks() { return tracks; }; private final ArrayList tracks = new ArrayList( /*initial capacity*/8 ); /** Wraps a sequence of name/value pairs to form a proper JSON object, after removing * any trailing comma (,). * * @see #appendJSON(StringBuilder) */ void wrapJSON( final StringBuilder b ) { b.insert( 0, '{' ); final int end = b.length() - 1; if( b.charAt(end) == ',' ) b.deleteCharAt( end ); // remove trailing comma b.append( '}' ); } // - H a s - H a n d l e r s ---------------------------------------------------------- public void fireEvent( final GwtEvent e ) { if( isFiring ) throw new IllegalStateException( "overlap in event dispatch" ); // per SuppressableSource prompter isFiring = true; final EventBus bus = GWTX.i().bus(); prompter.setSuppressed( false ); bus.fireEventFromSource( e, prompter ); if( !prompter.isSuppressed() ) bus.fireEventFromSource( e, Stage.this ); isFiring = false; } private boolean isFiring; // - O b j e c t ---------------------------------------------------------------------- public @Override final String toString() { final StringBuilder b = GWTX.stringBuilderClear(); appendJSON( b ); wrapJSON( b ); return b.toString(); } // - T h e a t r e - P a g e ---------------------------------------------------------- public String pageLocation() { return pageLocation; } private final String pageLocation = URIX.fragmentStripped( Document.get().getURL() ); // ` a c t o r ` n a m e `````````````````````````````````````````````````````````````` /** {@inheritDoc} The effective value is bound via the {@linkplain GWTX#bus() event * bus} to property name "actorName". * * @see #setActorName(String) * @see votorola.a.voter.IDPair#username() */ public String getActorName() { return actorName == null? defaultActorName: actorName; } private String actorName; /** Sets the username of the actor who is shown, or effectively resets it to the * default. Call this method from the global initialization function * voGWTConfig.s_gwt_stage_Stage_{@linkplain * TheatreInitializer#initFrom(Stage,boolean) initFrom} or {@linkplain * TheatreInitializer#initTo(Stage,TheatrePage) initTo} like this for * example:
   s_gwt_stage_Stage_setActorName( 'Joe-GmailCom' ); // otherwise null
* * @param newActorName the name to set, or null to effectively reset it to * the {@linkplain #getDefaultActorName() default name}. * * @see #getActorName() */ public static @GWTInitCallback void setActorName( final String newActorName ) { assert instance.initStep instanceof InitStarted: "initializers use initializerRegistry"; final String old = instance.actorName; instance.actorName = newActorName; instance.maybeFire( "actorName", old, newActorName, instance.defaultActorName ); } /** @see #getActorName() * @see #setDefaultActorName(String) */ public String getDefaultActorName() { return defaultActorName; } private String defaultActorName; /** Sets the username of the default actor, firing a change event if it affects * the bound property "actorName". Call this method from the global * initialization function voGWTConfig.s_gwt_stage_Stage_{@linkplain * TheatreInitializer#initFrom(Stage,boolean) initFrom} or {@linkplain * TheatreInitializer#initTo(Stage,TheatrePage) initTo} like this for * example:
   s_gwt_stage_Stage_setDefaultActorName( 'Joe-GmailCom' ); // otherwise null
* * @see #getDefaultActorName() */ public static @GWTInitCallback void setDefaultActorName( final String newDefault ) { assert instance.initStep instanceof InitStarted: "initializers use initializerRegistry"; final String old = instance.defaultActorName; instance.defaultActorName = newDefault; maybeFireDefault( "actorName", old, newDefault, instance.actorName ); } { stateChunkers.add( new StateChunker() { { init(); } void appendJSON( final StringBuilder b ) { appendJSON( b, "actorName", actorName ); appendJSON( b, "defaultActorName", defaultActorName ); } native void exposeJS() /*-{ $wnd.s_gwt_stage_Stage_setActorName = $entry( @votorola.s.gwt.stage.Stage::setActorName(Ljava/lang/String;) ); $wnd.s_gwt_stage_Stage_setDefaultActorName = $entry( @votorola.s.gwt.stage.Stage::setDefaultActorName(Ljava/lang/String;) ); }-*/; void restore( final TheatrePageJS state ) { setActorName( state._getString( "actorName" )); // bypass defaulting setDefaultActorName( state.getDefaultActorName() ); } }); } // ` d i f f e r e n c e `````````````````````````````````````````````````````````````` /** {@inheritDoc} The difference may be {@linkplain * votorola.s.gwt.stage.link.NominalDifferenceTargeter#NOMINAL_DIFF nominal}. The * effective value is bound via the {@linkplain GWTX#bus() event bus} to property * name "difference". * * @see #setDifference(DiffLook) */ public DiffLook getDifference() { return difference == null? defaultDifference: difference; } private DiffLook difference; /** Sets the key of the difference that is shown, or effectively resets it to the * default. * * @param k the key to set, or null to effectively reset it to the * {@linkplain #getDefaultDifference() default}. * * @see #getDifference() */ public void setDifference( final DiffLook k ) { assert initStep instanceof InitStarted: "initializers use initializerRegistry"; final DiffLook old = difference; difference = k; maybeFire( "difference", old, k, defaultDifference, DiffLookJS.EQUATOR ); } /** @see #getDifference() * @see #setDefaultDifference(DiffLook) */ public DiffLook getDefaultDifference() { return defaultDifference; } private DiffLook defaultDifference; /** Sets the key of the default difference, firing a change event if it affects * the bound property "difference". Call this method from the global * initialization function voGWTConfig.s_gwt_stage_Stage_{@linkplain * TheatreInitializer#initFrom(Stage,boolean) initFrom} or {@linkplain * TheatreInitializer#initTo(Stage,TheatrePage) initTo} like this for * example:
   s_gwt_stage_Stage_setDefaultDifference( // otherwise null
          *   {
          *      key: '5270.5284', selectand: 'b'
          *   });
* * @see #getDefaultDifference() */ public static @GWTInitCallback void setDefaultDifference( final DiffLook newDefault ) { assert instance.initStep instanceof InitStarted: "initializers use initializerRegistry"; final DiffLook old = instance.defaultDifference; instance.defaultDifference = newDefault; maybeFireDefault( "difference", old, newDefault, instance.difference, DiffLookJS.EQUATOR ); } { stateChunkers.add( new StateChunker() { { init(); } void appendJSON( final StringBuilder b ) { appendJSON( b, "difference", difference ); appendJSON( b, "defaultDifference", defaultDifference ); } private void appendJSON( final StringBuilder b, final String name, final DiffLook d ) { if( d == null || !d.toPersist() ) return; b.append( '"' ); b.append( name ); b.append( "\":{" ); appendJSON( b, "key", d.key() ); { final String selectand = d.selectand(); // "a" is the default in DiffLookJS if( !"a".equals( selectand )) appendJSON( b, "selectand", selectand ); } chopTrailingComma( b ); b.append( "}," ); } native void exposeJS() /*-{ $wnd.s_gwt_stage_Stage_setDefaultDifference = $entry( @votorola.s.gwt.stage.Stage::setDefaultDifference(Lvotorola/a/diff/DiffLook;) ); }-*/; void restore( final TheatrePageJS state ) { setDifference( (DiffLookJS)state._get( "difference" )); // bypass defaulting setDefaultDifference( state.getDefaultDifference() ); } }); } // ` m e s s a g e ```````````````````````````````````````````````````````````````````` /** {@inheritDoc} The value is bound via the {@linkplain GWTX#bus() event bus} to * property name "message". * * @see #setMessage(Message) */ public Message getMessage() { return message; } private Message message; /** Sets the message that is shown. * * @see #getMessage() */ public void setMessage( final Message newMessage ) { assert initStep instanceof InitStarted: "initializers use initializerRegistry"; if( MessageJS.EQUATOR.equals( message, newMessage )) return; message = newMessage; gun.schedule( new PropertyChange( "message" )); } { stateChunkers.add( new StateChunker() { { init(); } void appendJSON( final StringBuilder b ) { if( message == null ) return; b.append( "\"message\":{" ); appendJSON( b, "content", message.content() ); appendJSON( b, "location", message.location() ); chopTrailingComma( b ); b.append( "}," ); } void exposeJS() {} // no GWTInitCallback methods yet void restore( final TheatrePageJS state ) { setMessage( state.getMessage() ); // ok, unlike others it has no defaulting } }); } // ` p o l l ` n a m e ```````````````````````````````````````````````````````````````` /** {@inheritDoc} The effective value is bound via the {@linkplain GWTX#bus() event * bus} to property name "pollName". * * @see Category:Poll * @see #setPollName(String) */ public String getPollName() { return pollName == null? defaultPollName: pollName; } private String pollName; /** Sets the name of the poll that is shown, or effectively resets it to the * default. * * @param newPollName the new name to set, or null to effectively reset it to * the {@linkplain #getDefaultPollName() default name}. * * @see #getPollName() */ public void setPollName( String newPollName ) { assert initStep instanceof InitStarted: "initializers use initializerRegistry"; final String old = pollName; pollName = newPollName; maybeFire( "pollName", old, newPollName, defaultPollName ); } /** @see #getPollName() * @see #setDefaultPollName(String) */ public String getDefaultPollName() { return defaultPollName; } private String defaultPollName; /** Sets the default poll name, firing a change event if it affects the bound * property "pollName". Call this method from the global initialization function * voGWTConfig.s_gwt_stage_Stage_{@linkplain * TheatreInitializer#initFrom(Stage,boolean) initFrom} or {@linkplain * TheatreInitializer#initTo(Stage,TheatrePage) initTo} like this for * example:
   s_gwt_stage_Stage_setDefaultPollName( 'Sys/p/Sandbox' ); // otherwise null
* * @see #getDefaultPollName() */ public static @GWTInitCallback void setDefaultPollName( final String newDefault ) { assert instance.initStep instanceof InitStarted: "initializers use initializerRegistry"; final String old = instance.defaultPollName; instance.defaultPollName = newDefault; maybeFireDefault( "pollName", old, newDefault, instance.pollName ); } { stateChunkers.add( new StateChunker() { { init(); } void appendJSON( final StringBuilder b ) { appendJSON( b, "pollName", pollName ); appendJSON( b, "defaultPollName", defaultPollName ); } native void exposeJS() /*-{ $wnd.s_gwt_stage_Stage_setDefaultPollName = $entry( @votorola.s.gwt.stage.Stage::setDefaultPollName(Ljava/lang/String;) ); }-*/; void restore( final TheatrePageJS state ) { setPollName( state._getString( "pollName" )); // bypass defaulting setDefaultPollName( state.getDefaultPollName() ); } }); } // - W a r n i n g - S i n k ---------------------------------------------------------- /** @see #warnings() */ public void addWarning( final String warning ) { warnings.add( warning ); gun.schedule( new PropertyChange( "warnings" )); } /** A list of warnings for the user. Additions to the list are bound via the * {@linkplain GWTX#bus() event bus} to property name warnings. * * @see #addWarning(String) */ public List warnings() { return warnings; }; private final ArrayList warnings = new ArrayList( /*initial capacity*/4 ); // ==================================================================================== /** Utilities for dealing with a chunk of state that may persist across time and/or * pages and sites. */ static abstract class StateChunker { final void init() { exposeJS(); } /** Exposes the {@linkplain GWTInitCallback initialization callback methods} as * global JavaScript functions. */ abstract void exposeJS(); /** Appends to b a persistent form of the state consisting of one or * more JSON name/value pairs, each terminated by a comma (,). * * * @see http://json.org/ * @see #restore(TheatrePageJS) */ abstract void appendJSON( StringBuilder b ); // /** Appends to b a single JSON name/value pair in which the value is // * an integer. // */ // static void appendJSON( final StringBuilder b, final String name, final int value ) // { // b.append( '"' ); // b.append( name ); // b.append( "\":" ); // b.append( Integer.toString(value) ); // b.append( ',' ); // } /** Appends to b a single JSON name/value pair in which the value is * a string, but only if the value is non-null. */ static void appendJSON( final StringBuilder b, final String name, final String value ) { if( value == null ) return; b.append( '"' ); b.append( name ); b.append( "\":\"" ); b.append( value ); b.append( "\"," ); } /** Reads the state that was previously written and restores it to the stage. * * @see #appendJSON(StringBuilder) */ abstract void restore( TheatrePageJS state ); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static void chopTrailingComma( final StringBuilder b ) { final int end = b.length() - 1; assert b.charAt(end) == ','; b.deleteCharAt( end ); } private void flushForInitComplete() { gun.flush(); } // per TheatreInitializer.init*Complete private final DelayedEventGun gun = new DelayedEventGun( /*source*/Stage.this, CoalescingSchedulerS.DEFERRED ) { public @Override void schedule( final GwtEvent e ) { super.schedule( e ); if( e instanceof PropertyChange ) appendJSON_cache = null; } }; private void maybeFire( final String name, final Object oOld, final Object oNew, final Object oDefault ) { maybeFire( name, oOld, oNew, oDefault, ObjectX.EQUATOR ); } private void maybeFire( final String name, final T oOld, final T oNew, final T oDefault, Equator equator ) { final String nameToFire; if( oOld == null ) { if( oNew == null ) return; if( equator.equals( oNew, oDefault )) nameToFire = name + " masked"; else nameToFire = name; } else if( oNew == null ) { if( oOld == null ) return; if( equator.equals( oOld, oDefault )) nameToFire = name + " masked"; else nameToFire = name; } else { if( equator.equals( oNew, oOld )) return; nameToFire = name; } gun.schedule( new PropertyChange( nameToFire )); } private static void maybeFireDefault( final String name, final Object oOld, final Object oNew, final Object oValue ) { maybeFireDefault( name, oOld, oNew, oValue, ObjectX.EQUATOR ); } private static void maybeFireDefault( final String name, final T oOld, final T oNew, final T oValue, Equator equator ) { final String nameToFire; if( equator.equals( oOld, oNew )) return; if( oValue == null ) nameToFire = name; else nameToFire = name + " masked"; instance.gun.schedule( new PropertyChange( nameToFire )); } private TheatrePageJS referrer; // ==================================================================================== private final class Init { boolean isImmediatelyComplete; // as in TheatreInitializer.init*Complete, that is boolean isReferencePending = true; // till setReference boolean isUltimatelyComplete() { return isImmediatelyComplete && !isReferencePending; } ReferrerRelayer referrerRelayer; // final after set void setReference( ReferrerRelayer _referrerRelayer, boolean _isReferencePending ) { referrerRelayer = _referrerRelayer; isReferencePending = _isReferencePending; assert referrer == null || !isReferencePending; // what's already here is not pending } LinkedList theatreInitializers = new LinkedList(); } // ==================================================================================== private final class InitByRestore extends InitStarted { void add( final TheatreInitializer i, final boolean isReferencePending ) { if( referrer == null ) // cf. Restorer.init() { i.initFrom( Stage.this, isReferencePending ); flushForInitComplete(); i.initFromComplete( Stage.this, isReferencePending ); } else // referrer resolved late, step now effectively InitFromScratch { assert !isReferencePending; // what's already here is not pending i.initTo( Stage.this, referrer ); flushForInitComplete(); i.initToComplete( Stage.this, isReferencePending ); } } } // ==================================================================================== private final class InitFromScratch extends InitStarted { void add( final TheatreInitializer i, final boolean isReferencePending ) { // cf. ScratchInitializer.init() if( isReferencePending ) { assert referrer == null; // what's already here is not pending i.initTo( Stage.this ); } else i.initTo( Stage.this, referrer ); flushForInitComplete(); i.initToComplete( Stage.this, isReferencePending ); } } // ==================================================================================== private final class InitPending extends InitStep { void add( final TheatreInitializer i ) { init.theatreInitializers.add( i ); } } // ==================================================================================== private abstract class InitStarted extends InitStep { final void add( final TheatreInitializer i ) { final boolean isReferencePending; if( init == null ) isReferencePending = false; else { isReferencePending = init.isReferencePending; init.theatreInitializers.add( i ); } add( i, isReferencePending ); } abstract void add( TheatreInitializer i, boolean isReferencePending ); } // ==================================================================================== private abstract class InitStep { abstract void add( TheatreInitializer i ); } // ==================================================================================== /** A restorer of stage state for this page. Either this or ScratchInitializer runs, * not both. */ private final class Restorer { Restorer( Storer _storer ) { storer = _storer; } void init( final String pageStateJSON ) { initStep = new InitByRestore(); final TheatrePageJS state = JsonUtils.unsafeEval( pageStateJSON ); // stored by Storer, safe to use fast eval for( StateChunker chunker: stateChunkers ) chunker.restore( state ); storer.isChanged = false; final boolean isReferencePending = init.isReferencePending; initFromJS( isReferencePending ); final List inits = init.theatreInitializers; for( TheatreInitializer i: inits ) i.initFrom( Stage.this, isReferencePending ); flushForInitComplete(); initFromJSComplete( isReferencePending ); for( TheatreInitializer i: inits ) i.initFromComplete( Stage.this, isReferencePending ); init3(); } private native void initFromJS( boolean isReferencePending ) /*-{ var f; try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initFrom; } catch( e ) {} // in case parents undefined if( f ) f( isReferencePending ); }-*/; private native void initFromJSComplete( boolean isReferencePending ) /*-{ var f; try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initFromComplete; } catch( e ) {} // in case parents undefined if( f ) f( isReferencePending ); }-*/; private final Storer storer; } // ==================================================================================== /** An initializer of original stage state as opposed to stored state. Either this or * Restorer runs, not both. */ private final class ScratchInitializer { void init() // cf. InitByRestore.add, InitFromScratch.add { initStep = new InitFromScratch(); final boolean isReferencePending = init.isReferencePending; initToJS( referrer, isReferencePending ); final List inits = init.theatreInitializers; if( isReferencePending ) { assert referrer == null; // what's already here is not pending for( TheatreInitializer i: inits ) i.initTo( Stage.this ); } else for( TheatreInitializer i: inits ) i.initTo( Stage.this, referrer ); flushForInitComplete(); initToJSComplete( isReferencePending ); for( TheatreInitializer i: inits ) i.initToComplete( Stage.this, isReferencePending ); init3(); } private native void initToJS( TheatrePageJS referrer, boolean isReferencePending ) /*-{ var f; try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initTo; } catch( e ) {} // in case parents undefined if( f ) f( referrer, isReferencePending ); }-*/; private native void initToJSComplete( boolean isReferencePending ) /*-{ var f; try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initToComplete; } catch( e ) {} // in case parents undefined if( f ) f( isReferencePending ); }-*/; } // ==================================================================================== /** A storer of stage state for this page. */ private final class Storer implements PageHideHandler, PropertyChangeHandler { Storer( final Storage _store, final String _storageKey ) { store = _store; storageKey = _storageKey; GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/Stage.this, Storer.this ); // no need to unregister, registry does not outlive this listener Window.addPageHideHandler( Storer.this ); // no need to unregister, " // Would rather listen for permanent destruction of the document, whether by // direct unload or later removal from the memory cache. Mozilla [1] says // XPCOM is required for this [2], but maybe it is restricted to extensions. // In any case, a cross-browser solution would appear to be difficult. // // [1] https://developer.mozilla.org/En/Working_with_BFCache // [2] https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIObserver // https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsISupportsPRUint64 // Maybe also see: https://developer.mozilla.org/en/Observer_Notifications } boolean isChanged; // since last sync of state/store by Storer or Restorer private final String storageKey; private final Storage store; // - P a g e - H i d e - H a n d l e r -------------------------------------------- public void onPageHide( PageHideEvent _e ) { if( !isChanged ) return; store.setItem( storageKey, Stage.this.toString() ); isChanged = false; } // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- public void onPropertyChange( PropertyChange _e ) { isChanged = true; } } }