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