001package 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.
002
003import com.google.gwt.core.client.*;
004import com.google.gwt.dom.client.*;
005import com.google.gwt.event.shared.GwtEvent;
006import com.google.gwt.event.shared.HasHandlers;
007import com.google.gwt.storage.client.Storage;
008import com.google.gwt.user.client.*;
009import com.google.web.bindery.event.shared.EventBus;
010import java.util.*;
011import votorola.a.diff.*;
012import votorola.a.web.gwt.*;
013import votorola.g.lang.*;
014import votorola.g.net.*;
015import votorola.g.web.gwt.*;
016import votorola.g.web.gwt.event.*;
017import votorola.s.gwt.stage.light.LightBank;
018
019
020/** The <a href='http://reluk.ca/w/Stuff:Votorola/t/Crossforum_Theatre' target='_top'>Crossforum Theatre</a>
021  * stage.  The purpose of the stage is to support
022  * the decoration of web pages (scenes) with specialized views and controls (props) for
023  * the purpose of <a href='http://reluk.ca/w/Stuff:Broad-based_system_guidance'
024  * target='_top'>broad-based system guidance</a>.  Any web page may be "staged" in this
025  * manner.  The props are typically deployed in containers known as "{@linkplain Track
026  * tracks}" that are layed out along the edges of the browser viewport.  See {@linkplain
027  * StageV StageV} for layout examples.  The stage itself provides support to the props in
028  * the form of common variables that model the core state of the application.  This
029  * enables the props to function in a coordinated manner while remaining independent of
030  * each other, which in turn frees the user to make a personal choice of props based on
031  * need or preference.  Although such personal customization is not yet supported by
032  * current implementations of the stage, it remains a design requirement for the future.
033  *
034  * <h3 id='persistPage'>Persistence of state in a single page</h3>
035  *
036  * <p>The {@linkplain TheatrePage staged state} of each page is persisted to ensure that
037  * changes by the user (selections and so forth) are not lost due to page reload during
038  * back and forth navigation, refresh, and so forth.  (A memory cache such as Firefox's
039  * BFCache, or WebKit's page cache will also do this, but is not available for all
040  * browsers, nor enabled for all pages.)  For this to work correctly, scenes and props
041  * must initialize on page load vis-a-vis the stage only via {@linkplain
042  * TheatreInitializer theatre initializers} that are {@linkplain
043  * #addInitializer(TheatreInitializer) registered with the stage}.</p>
044  *
045  * <p>To clear the persisted state and load a pristine page either (A) close and reopen
046  * the window or tab, or otherwise restart the browsing session; or (B) clear the web
047  * store (also known as DOM or HTML5 storage) by whatever means the browser provides
048  * (e.g. entering <code>sessionStorage.clear()</code> in the Firebug console); or (C) use
049  * the following procedure to clear the state for a single page: (1) ensure the URL has a
050  * fragment '#' delimiter by appending it if necessary; (2) also append "voStage.clear"
051  * to the end of the URL; (3) press return as if to navigate to that fragment; and
052  * finally (4) reload the page.  For example here's a URL in its original form and with
053  * the necessary appendage:</p><pre
054  *
055 *>   http://somewhere.com/fu#bar
056  *   http://somewhere.com/fu#bar&voStage.clear</pre>
057  *
058  * <h3 id='persistCrossPage'>Persistence of state across pages</h3>
059  *
060  * <p>The stage provides a mechanism for the selective transfer of state during transit
061  * from one page (referrer) to another via hypertext links.  This allows user selections
062  * and such to persist across pages.  This persistence is controlled by the props that
063  * depend upon it via the {@linkplain TheatreInitializer#initTo(Stage,TheatrePage)
064  * initTo}(stage,referrer) methods of their registered initializers.</p>
065  *
066  * <h3>State changes during ordinary use</h3>
067  *
068  * <p>Subsequent state changes such as those initiated by the user are generally expected
069  * as late as the "finally" phase of each event dispatch.  The associated change events
070  * are then dispatched in the "deferred" phase where they appear atomic regardless of the
071  * number of state variables involved.  There is a minor exception to this rule: late
072  * registration of an initializer (i) will nevertheless flush pending events immediately,
073  * as stipulated by i.{@linkplain TheatreInitializer#initFromComplete(Stage,boolean)
074  * initFromComplete} and {@linkplain TheatreInitializer#initToComplete(Stage,boolean)
075  * initToComplete}.</p>
076  *
077  * <p>Events are propagated via the {@linkplain GWTX#bus() common event bus}.  Each event
078  * is pre-dispatched from the suppressable {@linkplain #prompter() prompter} first.  The
079  * event is cancelled if it gets suppressed, otherwise it is re-dispatched from the
080  * ordinary source.</p>
081  *
082  * <h3 id='mask'>Defaulting properties and masked changes</h3>
083  *
084  * <p>A defaulting property is one that has a default mechanism, such as "{@linkplain
085  * #getDifference() difference}".  A defaulting property that changes from null to the
086  * default value, or from the default value to null, exhibits no effective change in its
087  * value.  The value remains at the default and the change is said to be masked.  No
088  * change event "NAME" is fired in this case, but rather a change event "NAME
089  * masked".</p>
090  */
091public final class Stage implements HasHandlers, TheatrePage, WarningSink
092{
093
094
095    /** Creates the {@linkplain #i() single instance} of Stage.
096      */
097    Stage()
098    {
099        tracks.add( new votorola.s.gwt.stage.link.LinkTrack() );
100        tracks.add( new votorola.s.gwt.stage.poll.Polltrack() );
101     // tracks.add( new votorola.s.gwt.stage.talk.TalkTrack() );
102     /// pending CWFIX, http://mail.zelea.com/list/votorola/2013-June/001763.html
103        tracks.add( new votorola.s.gwt.stage.vote.VoteTrack() );
104    }
105
106
107
108    /** The single instance of Stage.
109      */
110    public static Stage i() { return instance; }
111
112
113        private static Stage instance;
114
115        {
116            if( instance != null ) throw new IllegalStateException();
117
118            instance = Stage.this;
119        }
120
121
122
123    private Init init = new Init(); // nulled after init.isUltimatelyComplete()
124
125
126        private InitStep initStep = new InitPending();
127          // no change of stage state expected till step transits from pending
128
129
130
131    /** Begins initialization of the stage.
132      */
133    void init0() { new ReferrerRelayer().resolveReferrer( Stage.this ); }
134      // called by StageMod.execute
135
136
137
138    /** Continues initialization of the stage.
139      *
140      *     @param _isReferencePending whether the referrer (if any) remains to be
141      *       resolved asynchronously.  When true, _referrer must be null.
142      */
143    void init1( TheatrePageJS _referrer, ReferrerRelayer _referrerRelayer,
144      boolean _isReferencePending )
145    {
146        referrer = _referrer;
147        init.setReference( _referrerRelayer, _isReferencePending );
148        Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand()
149        {
150            // Deferral lays a trap for informal initializers that execute out of turn and
151            // thereby risk the integrity of state restoration.  It has no other purpose.
152            public void execute() { init2a_async(); }
153        });
154    }
155
156
157
158    private void init2a_async() // unknown order vs. init2b_async
159    {
160        final Storage store = Storage.getSessionStorageIfSupported();
161          // The session store is open to this script even if its host or port of origin
162          // differs from that of the page (Firefox 9, Chrome 18).  Apparently it is not
163          // subject to local storage's "same origin" restrictions of "scheme/host/port
164          // tuple" [1], or IndexedDB's equivalent "tuple (scheme, host, port)" [2].
165          //
166          // [1] http://www.w3.org/TR/webstorage/#security-localStorage
167          // [2] http://www.w3.org/TR/html5/origin-0.html#origin-0
168          //     as referenced from http://www.w3.org/TR/IndexedDB/
169        if( store != null )
170        {
171            final String storageKey = getClass().getName() + " state " + pageLocation;
172            final Storer storer = new Storer( store, storageKey );
173            if( referrer == null )
174            {
175                final String loc = Document.get().getURL();
176                final boolean hasFragment = loc.length() > pageLocation.length();
177                if( !hasFragment || loc.endsWith("voStage.clear") )
178                {
179                    final String pageStateJSON = store.getItem( storageKey );
180                    if( pageStateJSON != null )
181                    {
182                        new Restorer(storer).init( pageStateJSON );
183                        return;
184                    }
185                }
186            }
187        }
188
189        new ScratchInitializer().init();
190    }
191
192
193
194    /** Continues initialization of the stage in the case of a deferred referrer
195      * resolution.
196      */
197    void init2b_async( final TheatrePageJS latelyResolvedReferrer ) // unknown order vs. init2a_async
198    {
199        Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand()
200        {
201            // Scheduled otherwise init2b_async can somehow interpose itself between
202            // init2a_async - init3.  That should be impossible, but printouts in devmode
203            // show the iterposition (GWT 2.4.0+mca.1, Firefox 9) and it explains a bug
204            // simultaneously observed (resolved referrer is dropped).
205            public void execute()
206            {
207                assert init.isReferencePending;
208                init.isReferencePending = false;
209                referrer = latelyResolvedReferrer;
210                if( !init.isImmediatelyComplete ) return;
211                  // init2a_async still pending, let it do the work
212
213                init2bJS( latelyResolvedReferrer );
214                for( TheatreInitializer i: init.theatreInitializers )
215                {
216                    i.initUltimately( Stage.this, latelyResolvedReferrer );
217                }
218                if( init.isUltimatelyComplete() ) init = null; // actually, no if about it
219            }
220        });
221    }
222
223
224
225    private native void init2bJS( TheatrePageJS referrer )
226    /*-{
227        var f;
228        try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initUltimately; } catch( e ) {} // in case parents undefined
229        if( f ) f( referrer );
230    }-*/;
231
232
233
234    private void init3()
235    {
236        init.referrerRelayer.activate();
237        init.isImmediatelyComplete = true;
238        if( init.isUltimatelyComplete() ) init = null;
239    }
240
241
242
243   // ` e a r l y ````````````````````````````````````````````````````````````````````````
244
245
246    /** The collection of all state chunkers.
247      */
248    private final LinkedList<StateChunker> stateChunkers = new LinkedList<StateChunker>();
249
250
251
252   // ------------------------------------------------------------------------------------
253
254
255    /** Adds a theatre initializer to be run either immediately or later, depending on the
256      * current state of initialization.  Initializers are automatically removed after
257      * they are run.
258      */
259    public void addInitializer( final TheatreInitializer i ) { initStep.add( i ); }
260
261
262
263    /** Appends a string that encodes the state of this stage as a sequence of JSON
264      * name/value pairs suitable for underwriting an instance of {@linkplain
265      * TheatrePageJS TheatrePageJS}, all but for the pageLocation and the surrounding
266      * curly braces.  The value is effectively bound via the {@linkplain GWTX#bus() event
267      * bus} to all named properties "NAME" and "NAME masked" of the stage.  An event of
268      * either form signals a probable change to the JSON value, subject only to the
269      * possibility that multiple changes might cancel each other by the time the events
270      * are fired.
271      *
272      *     @see #wrapJSON(StringBuilder)
273      *     @see <a href='#mask'>Defaulting properties and masked changes</a>
274      */
275    void appendJSON( final StringBuilder b )
276    {
277        if( appendJSON_cache == null )
278        {
279            final int c = b.length();
280            for( StateChunker chunker: stateChunkers ) chunker.appendJSON( b );
281            appendJSON_cache = b.substring( c );
282        }
283        else b.append( appendJSON_cache );
284    }
285
286
287        private String appendJSON_cache; // cleared eagerly in gun
288
289
290
291    /** Answers whether the page is a position draft to be decorated by difference
292      * shadows, once the necessary differences are fetched from the server.  The value
293      * may only transit from false to true and is bound via the {@linkplain GWTX#bus()
294      * event bus} to property name <tt>differencesShadowed</tt>.  When true, the style
295      * class <tt>voDifferencesShadowed</tt> is set on the page body.
296      *
297      *     @see #setDifferencesShadowed()
298      *     @see votorola.s.gwt.mediawiki.DifferenceShadows
299      *     @see <a href='http://reluk.ca/w/Category:Draft' target='_top'>Category:Draft</a>
300      */
301    public boolean areDifferencesShadowed() { return areDifferencesShadowed; };
302
303
304        private boolean areDifferencesShadowed;
305
306
307        /** Specifies that the page is a position draft to be decorated by difference
308          * shadows.
309          *
310          *     @see #areDifferencesShadowed()
311          */
312        public void setDifferencesShadowed()
313        {
314            if( areDifferencesShadowed ) return;
315
316            areDifferencesShadowed = true;
317            gun.schedule( new PropertyChange( "differencesShadowed" ));
318            gun.phaser().schedule( gun.baseScheduler(), new Scheduler.ScheduledCommand()
319            {
320                public void execute()
321                {
322                    // in the same phase with event dispatch for smoother rendering in
323                    // case a listener makes disruptive style changes of its own
324                    Document.get().getBody().addClassName( "voDifferencesShadowed" );
325                }
326            });
327        }
328
329
330
331    /** The light bank for this stage.
332      */
333    public LightBank lightBank() { return lightBank; }
334
335
336        private final LightBank lightBank = new LightBank();
337
338
339
340    /** The source from which stage events are pre-dispatched.  Most handlers will name
341      * the stage itself as the event source.  Only those handlers that suppress events
342      * should name the prompter.
343      *
344      *     @see GWTX#bus()
345      */
346    public SuppressableSource prompter() { return prompter; }
347
348
349        private final SuppressableSource prompter = new SuppressableSource();
350
351
352
353    /** The list of all tracks on stage.
354      */
355    public List<Track> tracks() { return tracks; };
356
357
358        private final ArrayList<Track> tracks = new ArrayList<Track>( /*initial capacity*/8 );
359
360
361
362    /** Wraps a sequence of name/value pairs to form a proper JSON object, after removing
363      * any trailing comma (,).
364      *
365      *     @see #appendJSON(StringBuilder)
366      */
367    void wrapJSON( final StringBuilder b )
368    {
369        b.insert( 0, '{' );
370        final int end = b.length() - 1;
371        if( b.charAt(end) == ',' ) b.deleteCharAt( end ); // remove trailing comma
372        b.append( '}' );
373    }
374
375
376
377   // - H a s - H a n d l e r s ----------------------------------------------------------
378
379
380    public void fireEvent( final GwtEvent<?> e )
381    {
382        if( isFiring ) throw new IllegalStateException( "overlap in event dispatch" ); // per SuppressableSource prompter
383
384        isFiring = true;
385        final EventBus bus = GWTX.i().bus();
386        prompter.setSuppressed( false );
387        bus.fireEventFromSource( e, prompter );
388        if( !prompter.isSuppressed() ) bus.fireEventFromSource( e, Stage.this );
389        isFiring = false;
390    }
391
392
393        private boolean isFiring;
394
395
396
397   // - O b j e c t ----------------------------------------------------------------------
398
399
400    public @Override final String toString()
401    {
402        final StringBuilder b = GWTX.stringBuilderClear();
403        appendJSON( b );
404        wrapJSON( b );
405        return b.toString();
406    }
407
408
409
410   // - T h e a t r e - P a g e ----------------------------------------------------------
411
412
413    public String pageLocation() { return pageLocation; }
414
415
416        private final String pageLocation = URIX.fragmentStripped( Document.get().getURL() );
417
418
419
420   // ` a c t o r ` n a m e ``````````````````````````````````````````````````````````````
421
422
423    /** {@inheritDoc} The effective value is bound via the {@linkplain GWTX#bus() event
424      * bus} to property name "actorName".
425      *
426      *     @see #setActorName(String)
427      *     @see votorola.a.voter.IDPair#username()
428      */
429    public String getActorName() { return actorName == null? defaultActorName: actorName; }
430
431
432        private String actorName;
433
434
435        /** Sets the username of the actor who is shown, or effectively resets it to the
436          * default.  Call this method from the global initialization function
437          * voGWTConfig.s_gwt_stage_Stage_{@linkplain
438          * TheatreInitializer#initFrom(Stage,boolean) initFrom} or {@linkplain
439          * TheatreInitializer#initTo(Stage,TheatrePage) initTo} like this for
440          * example:<pre
441          *
442         *>   s_gwt_stage_Stage_setActorName( 'Joe-GmailCom' ); // otherwise null</pre>
443          *
444          *     @param newActorName the name to set, or null to effectively reset it to
445          *       the {@linkplain #getDefaultActorName() default name}.
446          *
447          *     @see #getActorName()
448          */
449        public static @GWTInitCallback void setActorName( final String newActorName )
450        {
451            assert instance.initStep instanceof InitStarted: "initializers use initializerRegistry";
452            final String old = instance.actorName;
453            instance.actorName = newActorName;
454            instance.maybeFire( "actorName", old, newActorName, instance.defaultActorName );
455        }
456
457
458
459    /** @see #getActorName()
460      * @see #setDefaultActorName(String)
461      */
462    public String getDefaultActorName() { return defaultActorName; }
463
464
465        private String defaultActorName;
466
467
468        /** Sets the username of the default actor, firing a change event if it affects
469          * the bound property "actorName".  Call this method from the global
470          * initialization function voGWTConfig.s_gwt_stage_Stage_{@linkplain
471          * TheatreInitializer#initFrom(Stage,boolean) initFrom} or {@linkplain
472          * TheatreInitializer#initTo(Stage,TheatrePage) initTo} like this for
473          * example:<pre
474          *
475         *>   s_gwt_stage_Stage_setDefaultActorName( 'Joe-GmailCom' ); // otherwise null</pre>
476          *
477          *     @see #getDefaultActorName()
478          */
479        public static @GWTInitCallback void setDefaultActorName( final String newDefault )
480        {
481            assert instance.initStep instanceof InitStarted: "initializers use initializerRegistry";
482            final String old = instance.defaultActorName;
483            instance.defaultActorName = newDefault;
484            maybeFireDefault( "actorName", old, newDefault, instance.actorName );
485        }
486
487
488
489    {
490        stateChunkers.add( new StateChunker()
491        {
492            { init(); }
493
494            void appendJSON( final StringBuilder b )
495            {
496                appendJSON( b, "actorName", actorName );
497                appendJSON( b, "defaultActorName", defaultActorName );
498            }
499
500            native void exposeJS()
501            /*-{
502                $wnd.s_gwt_stage_Stage_setActorName = $entry(
503                  @votorola.s.gwt.stage.Stage::setActorName(Ljava/lang/String;) );
504                $wnd.s_gwt_stage_Stage_setDefaultActorName = $entry(
505                  @votorola.s.gwt.stage.Stage::setDefaultActorName(Ljava/lang/String;) );
506            }-*/;
507
508            void restore( final TheatrePageJS state )
509            {
510                setActorName( state._getString( "actorName" )); // bypass defaulting
511                setDefaultActorName( state.getDefaultActorName() );
512            }
513        });
514    }
515
516
517
518   // ` d i f f e r e n c e ``````````````````````````````````````````````````````````````
519
520
521    /** {@inheritDoc} The difference may be {@linkplain
522      * votorola.s.gwt.stage.link.NominalDifferenceTargeter#NOMINAL_DIFF nominal}.  The
523      * effective value is bound via the {@linkplain GWTX#bus() event bus} to property
524      * name "difference".
525      *
526      *     @see #setDifference(DiffLook)
527      */
528    public DiffLook getDifference() { return difference == null? defaultDifference: difference; }
529
530
531        private DiffLook difference;
532
533
534        /** Sets the key of the difference that is shown, or effectively resets it to the
535          * default.
536          *
537          *     @param k the key to set, or null to effectively reset it to the
538          *       {@linkplain #getDefaultDifference() default}.
539          *
540          *     @see #getDifference()
541          */
542        public void setDifference( final DiffLook k )
543        {
544            assert initStep instanceof InitStarted: "initializers use initializerRegistry";
545            final DiffLook old = difference;
546            difference = k;
547            maybeFire( "difference", old, k, defaultDifference, DiffLookJS.EQUATOR );
548        }
549
550
551
552    /** @see #getDifference()
553      * @see #setDefaultDifference(DiffLook)
554      */
555    public DiffLook getDefaultDifference() { return defaultDifference; }
556
557
558        private DiffLook defaultDifference;
559
560
561        /** Sets the key of the default difference, firing a change event if it affects
562          * the bound property "difference".  Call this method from the global
563          * initialization function voGWTConfig.s_gwt_stage_Stage_{@linkplain
564          * TheatreInitializer#initFrom(Stage,boolean) initFrom} or {@linkplain
565          * TheatreInitializer#initTo(Stage,TheatrePage) initTo} like this for
566          * example:<pre
567          *
568         *>   s_gwt_stage_Stage_setDefaultDifference( // otherwise null
569          *   {
570          *      key: '5270.5284', selectand: 'b'
571          *   });</pre>
572          *
573          *     @see #getDefaultDifference()
574          */
575        public static @GWTInitCallback void setDefaultDifference( final DiffLook newDefault )
576        {
577            assert instance.initStep instanceof InitStarted: "initializers use initializerRegistry";
578            final DiffLook old = instance.defaultDifference;
579            instance.defaultDifference = newDefault;
580            maybeFireDefault( "difference", old, newDefault, instance.difference,
581              DiffLookJS.EQUATOR );
582        }
583
584
585
586    {
587        stateChunkers.add( new StateChunker()
588        {
589            { init(); }
590
591            void appendJSON( final StringBuilder b )
592            {
593                appendJSON( b, "difference", difference );
594                appendJSON( b, "defaultDifference", defaultDifference );
595            }
596
597            private void appendJSON( final StringBuilder b, final String name, final DiffLook d )
598            {
599                if( d == null || !d.toPersist()  ) return;
600
601                b.append( '"' );
602                b.append( name );
603                b.append( "\":{" );
604                appendJSON( b, "key", d.key() );
605                {
606                    final String selectand = d.selectand(); // "a" is the default in DiffLookJS
607                    if( !"a".equals( selectand )) appendJSON( b, "selectand", selectand );
608                }
609                chopTrailingComma( b );
610                b.append( "}," );
611            }
612
613            native void exposeJS()
614            /*-{
615                $wnd.s_gwt_stage_Stage_setDefaultDifference = $entry(
616                  @votorola.s.gwt.stage.Stage::setDefaultDifference(Lvotorola/a/diff/DiffLook;) );
617            }-*/;
618
619            void restore( final TheatrePageJS state )
620            {
621                setDifference( (DiffLookJS)state._get( "difference" )); // bypass defaulting
622                setDefaultDifference( state.getDefaultDifference() );
623            }
624        });
625    }
626
627
628
629   // ` m e s s a g e ````````````````````````````````````````````````````````````````````
630
631
632    /** {@inheritDoc} The value is bound via the {@linkplain GWTX#bus() event bus} to
633      * property name "message".
634      *
635      *     @see #setMessage(Message)
636      */
637    public Message getMessage() { return message; }
638
639
640        private Message message;
641
642
643        /** Sets the message that is shown.
644          *
645          *     @see #getMessage()
646          */
647        public void setMessage( final Message newMessage )
648        {
649            assert initStep instanceof InitStarted: "initializers use initializerRegistry";
650            if( MessageJS.EQUATOR.equals( message, newMessage )) return;
651
652            message = newMessage;
653            gun.schedule( new PropertyChange( "message" ));
654        }
655
656
657
658    {
659        stateChunkers.add( new StateChunker()
660        {
661            { init(); }
662
663            void appendJSON( final StringBuilder b )
664            {
665                if( message == null ) return;
666
667                b.append( "\"message\":{" );
668                appendJSON( b, "content", message.content() );
669                appendJSON( b, "location", message.location() );
670                chopTrailingComma( b );
671                b.append( "}," );
672            }
673
674            void exposeJS() {} // no GWTInitCallback methods yet
675
676            void restore( final TheatrePageJS state )
677            {
678                setMessage( state.getMessage() ); // ok, unlike others it has no defaulting
679            }
680        });
681    }
682
683
684
685   // ` p o l l ` n a m e ````````````````````````````````````````````````````````````````
686
687
688    /** {@inheritDoc} The effective value is bound via the {@linkplain GWTX#bus() event
689      * bus} to property name "pollName".
690      *
691      *     @see <a href='http://reluk.ca/w/Category:Poll'
692      *                        target='_top'>Category:Poll</a>
693      *     @see #setPollName(String)
694      */
695    public String getPollName() { return pollName == null? defaultPollName: pollName; }
696
697
698        private String pollName;
699
700
701        /** Sets the name of the poll that is shown, or effectively resets it to the
702          * default.
703          *
704          *     @param newPollName the new name to set, or null to effectively reset it to
705          *       the {@linkplain #getDefaultPollName() default name}.
706          *
707          *     @see #getPollName()
708          */
709        public void setPollName( String newPollName )
710        {
711            assert initStep instanceof InitStarted: "initializers use initializerRegistry";
712            final String old = pollName;
713            pollName = newPollName;
714            maybeFire( "pollName", old, newPollName, defaultPollName );
715        }
716
717
718        /** @see #getPollName()
719          * @see #setDefaultPollName(String)
720          */
721        public String getDefaultPollName() { return defaultPollName; }
722
723
724        private String defaultPollName;
725
726
727        /** Sets the default poll name, firing a change event if it affects the bound
728          * property "pollName".  Call this method from the global initialization function
729          * voGWTConfig.s_gwt_stage_Stage_{@linkplain
730          * TheatreInitializer#initFrom(Stage,boolean) initFrom} or {@linkplain
731          * TheatreInitializer#initTo(Stage,TheatrePage) initTo} like this for
732          * example:<pre
733          *
734         *>   s_gwt_stage_Stage_setDefaultPollName( 'Sys/p/Sandbox' ); // otherwise null</pre>
735          *
736          *     @see #getDefaultPollName()
737          */
738        public static @GWTInitCallback void setDefaultPollName( final String newDefault )
739        {
740            assert instance.initStep instanceof InitStarted: "initializers use initializerRegistry";
741            final String old = instance.defaultPollName;
742            instance.defaultPollName = newDefault;
743            maybeFireDefault( "pollName", old, newDefault, instance.pollName );
744        }
745
746
747
748    {
749        stateChunkers.add( new StateChunker()
750        {
751            { init(); }
752
753            void appendJSON( final StringBuilder b )
754            {
755                appendJSON( b, "pollName", pollName );
756                appendJSON( b, "defaultPollName", defaultPollName );
757            }
758
759            native void exposeJS()
760            /*-{
761                $wnd.s_gwt_stage_Stage_setDefaultPollName = $entry(
762                  @votorola.s.gwt.stage.Stage::setDefaultPollName(Ljava/lang/String;) );
763            }-*/;
764
765            void restore( final TheatrePageJS state )
766            {
767                setPollName( state._getString( "pollName" )); // bypass defaulting
768                setDefaultPollName( state.getDefaultPollName() );
769            }
770        });
771    }
772
773
774
775   // - W a r n i n g - S i n k ----------------------------------------------------------
776
777
778    /** @see #warnings()
779      */
780    public void addWarning( final String warning )
781    {
782        warnings.add( warning );
783        gun.schedule( new PropertyChange( "warnings" ));
784    }
785
786
787
788    /** A list of warnings for the user.  Additions to the list are bound via the
789      * {@linkplain GWTX#bus() event bus} to property name <tt>warnings</tt>.
790      *
791      *     @see #addWarning(String)
792      */
793    public List<String> warnings() { return warnings; };
794
795
796        private final ArrayList<String> warnings = new ArrayList<String>( /*initial capacity*/4 );
797
798
799
800   // ====================================================================================
801
802
803    /** Utilities for dealing with a chunk of state that may persist across time and/or
804      * pages and sites.
805      */
806    static abstract class StateChunker
807    {
808
809        final void init() { exposeJS(); }
810
811
812        /** Exposes the {@linkplain GWTInitCallback initialization callback methods} as
813          * global JavaScript functions.
814          */
815        abstract void exposeJS();
816
817
818        /** Appends to <code>b</code> a persistent form of the state consisting of one or
819          * more JSON name/value pairs, each terminated by a comma (,).
820          *
821          *
822          *     @see <a href='http://json.org/' target='_top'>http://json.org/</a>
823          *     @see #restore(TheatrePageJS)
824          */
825        abstract void appendJSON( StringBuilder b );
826
827
828     // /** Appends to <code>b</code> a single JSON name/value pair in which the value is
829     //   * an integer.
830     //   */
831     // static void appendJSON( final StringBuilder b, final String name, final int value )
832     // {
833     //     b.append( '"' );
834     //     b.append( name );
835     //     b.append( "\":" );
836     //     b.append( Integer.toString(value) );
837     //     b.append( ',' );
838     // }
839
840
841        /** Appends to <code>b</code> a single JSON name/value pair in which the value is
842          * a string, but only if the value is non-null.
843          */
844        static void appendJSON( final StringBuilder b, final String name, final String value )
845        {
846            if( value == null ) return;
847
848            b.append( '"' );
849            b.append( name );
850            b.append( "\":\"" );
851            b.append( value );
852            b.append( "\"," );
853        }
854
855
856        /** Reads the state that was previously written and restores it to the stage.
857          *
858          *     @see #appendJSON(StringBuilder)
859          */
860        abstract void restore( TheatrePageJS state );
861
862    }
863
864
865
866//// P r i v a t e ///////////////////////////////////////////////////////////////////////
867
868
869    private static void chopTrailingComma( final StringBuilder b )
870    {
871        final int end = b.length() - 1;
872        assert b.charAt(end) == ',';
873        b.deleteCharAt( end );
874    }
875
876
877
878    private void flushForInitComplete() { gun.flush(); } // per TheatreInitializer.init*Complete
879
880
881
882    private final DelayedEventGun gun = new DelayedEventGun( /*source*/Stage.this,
883      CoalescingSchedulerS.DEFERRED )
884    {
885        public @Override void schedule( final GwtEvent<?> e )
886        {
887            super.schedule( e );
888            if( e instanceof PropertyChange ) appendJSON_cache = null;
889        }
890    };
891
892
893
894    private void maybeFire( final String name, final Object oOld, final Object oNew,
895      final Object oDefault )
896    {
897        maybeFire( name, oOld, oNew, oDefault, ObjectX.EQUATOR );
898    }
899
900
901
902    private <T> void maybeFire( final String name, final T oOld, final T oNew, final T oDefault,
903      Equator<T> equator )
904    {
905        final String nameToFire;
906        if( oOld == null )
907        {
908            if( oNew == null ) return;
909
910            if( equator.equals( oNew, oDefault )) nameToFire = name + " masked";
911            else nameToFire = name;
912        }
913        else if( oNew == null )
914        {
915            if( oOld == null ) return;
916
917            if( equator.equals( oOld, oDefault )) nameToFire = name + " masked";
918            else nameToFire = name;
919        }
920        else
921        {
922            if( equator.equals( oNew, oOld )) return;
923
924            nameToFire = name;
925        }
926        gun.schedule( new PropertyChange( nameToFire ));
927    }
928
929
930
931    private static void maybeFireDefault( final String name, final Object oOld, final Object oNew,
932      final Object oValue )
933    {
934        maybeFireDefault( name, oOld, oNew, oValue, ObjectX.EQUATOR );
935    }
936
937
938
939    private static <T> void maybeFireDefault( final String name, final T oOld, final T oNew,
940      final T oValue, Equator<T> equator )
941    {
942        final String nameToFire;
943        if( equator.equals( oOld, oNew )) return;
944
945        if( oValue == null ) nameToFire = name;
946        else nameToFire = name + " masked";
947        instance.gun.schedule( new PropertyChange( nameToFire ));
948    }
949
950
951
952    private TheatrePageJS referrer;
953
954
955
956   // ====================================================================================
957
958
959    private final class Init
960    {
961
962        boolean isImmediatelyComplete; // as in TheatreInitializer.init*Complete, that is
963
964
965        boolean isReferencePending = true; // till setReference
966
967
968        boolean isUltimatelyComplete() { return isImmediatelyComplete && !isReferencePending; }
969
970
971        ReferrerRelayer referrerRelayer; // final after set
972
973
974        void setReference( ReferrerRelayer _referrerRelayer, boolean _isReferencePending )
975        {
976            referrerRelayer = _referrerRelayer;
977            isReferencePending = _isReferencePending;
978            assert referrer == null || !isReferencePending; // what's already here is not pending
979        }
980
981
982        LinkedList<TheatreInitializer> theatreInitializers = new LinkedList<TheatreInitializer>();
983
984    }
985
986
987
988   // ====================================================================================
989
990
991    private final class InitByRestore extends InitStarted
992    {
993        void add( final TheatreInitializer i, final boolean isReferencePending )
994        {
995            if( referrer == null ) // cf. Restorer.init()
996            {
997                i.initFrom( Stage.this, isReferencePending );
998                flushForInitComplete();
999                i.initFromComplete( Stage.this, isReferencePending );
1000            }
1001            else // referrer resolved late, step now effectively InitFromScratch
1002            {
1003                assert !isReferencePending; // what's already here is not pending
1004                i.initTo( Stage.this, referrer );
1005                flushForInitComplete();
1006                i.initToComplete( Stage.this, isReferencePending );
1007            }
1008        }
1009    }
1010
1011
1012
1013   // ====================================================================================
1014
1015
1016    private final class InitFromScratch extends InitStarted
1017    {
1018        void add( final TheatreInitializer i, final boolean isReferencePending )
1019        {
1020            // cf. ScratchInitializer.init()
1021            if( isReferencePending )
1022            {
1023                assert referrer == null; // what's already here is not pending
1024                i.initTo( Stage.this );
1025            }
1026            else i.initTo( Stage.this, referrer );
1027            flushForInitComplete();
1028            i.initToComplete( Stage.this, isReferencePending );
1029        }
1030    }
1031
1032
1033
1034   // ====================================================================================
1035
1036
1037    private final class InitPending extends InitStep
1038    {
1039        void add( final TheatreInitializer i ) { init.theatreInitializers.add( i ); }
1040    }
1041
1042
1043
1044   // ====================================================================================
1045
1046
1047    private abstract class InitStarted extends InitStep
1048    {
1049        final void add( final TheatreInitializer i )
1050        {
1051            final boolean isReferencePending;
1052            if( init == null ) isReferencePending = false;
1053            else
1054            {
1055                isReferencePending = init.isReferencePending;
1056                init.theatreInitializers.add( i );
1057            }
1058            add( i, isReferencePending );
1059        }
1060
1061        abstract void add( TheatreInitializer i, boolean isReferencePending );
1062    }
1063
1064
1065
1066   // ====================================================================================
1067
1068
1069    private abstract class InitStep { abstract void add( TheatreInitializer i ); }
1070
1071
1072
1073   // ====================================================================================
1074
1075
1076    /** A restorer of stage state for this page.  Either this or ScratchInitializer runs,
1077      * not both.
1078      */
1079    private final class Restorer
1080    {
1081
1082        Restorer( Storer _storer ) { storer = _storer; }
1083
1084
1085        void init( final String pageStateJSON )
1086        {
1087            initStep = new InitByRestore();
1088            final TheatrePageJS state = JsonUtils.unsafeEval( pageStateJSON );
1089              // stored by Storer, safe to use fast eval
1090            for( StateChunker chunker: stateChunkers ) chunker.restore( state );
1091            storer.isChanged = false;
1092            final boolean isReferencePending = init.isReferencePending;
1093            initFromJS( isReferencePending );
1094            final List<TheatreInitializer> inits = init.theatreInitializers;
1095            for( TheatreInitializer i: inits ) i.initFrom( Stage.this, isReferencePending );
1096            flushForInitComplete();
1097            initFromJSComplete( isReferencePending );
1098            for( TheatreInitializer i: inits ) i.initFromComplete( Stage.this, isReferencePending );
1099            init3();
1100        }
1101
1102
1103        private native void initFromJS( boolean isReferencePending )
1104        /*-{
1105            var f;
1106            try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initFrom; } catch( e ) {} // in case parents undefined
1107            if( f ) f( isReferencePending );
1108        }-*/;
1109
1110
1111        private native void initFromJSComplete( boolean isReferencePending )
1112        /*-{
1113            var f;
1114            try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initFromComplete; } catch( e ) {} // in case parents undefined
1115            if( f ) f( isReferencePending );
1116        }-*/;
1117
1118
1119        private final Storer storer;
1120
1121    }
1122
1123
1124
1125   // ====================================================================================
1126
1127
1128    /** An initializer of original stage state as opposed to stored state.  Either this or
1129      * Restorer runs, not both.
1130      */
1131    private final class ScratchInitializer
1132    {
1133
1134        void init() // cf. InitByRestore.add, InitFromScratch.add
1135        {
1136            initStep = new InitFromScratch();
1137            final boolean isReferencePending = init.isReferencePending;
1138            initToJS( referrer, isReferencePending );
1139            final List<TheatreInitializer> inits = init.theatreInitializers;
1140            if( isReferencePending )
1141            {
1142                assert referrer == null; // what's already here is not pending
1143                for( TheatreInitializer i: inits ) i.initTo( Stage.this );
1144            }
1145            else for( TheatreInitializer i: inits ) i.initTo( Stage.this, referrer );
1146            flushForInitComplete();
1147            initToJSComplete( isReferencePending );
1148            for( TheatreInitializer i: inits ) i.initToComplete( Stage.this, isReferencePending );
1149            init3();
1150        }
1151
1152
1153        private native void initToJS( TheatrePageJS referrer, boolean isReferencePending )
1154        /*-{
1155            var f;
1156            try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initTo; } catch( e ) {} // in case parents undefined
1157            if( f ) f( referrer, isReferencePending );
1158        }-*/;
1159
1160
1161        private native void initToJSComplete( boolean isReferencePending )
1162        /*-{
1163            var f;
1164            try{ f = $wnd.voGWTConfig.s_gwt_stage_Stage_initToComplete; } catch( e ) {} // in case parents undefined
1165            if( f ) f( isReferencePending );
1166        }-*/;
1167
1168    }
1169
1170
1171
1172   // ====================================================================================
1173
1174
1175    /** A storer of stage state for this page.
1176      */
1177    private final class Storer implements PageHideHandler, PropertyChangeHandler
1178    {
1179
1180        Storer( final Storage _store, final String _storageKey )
1181        {
1182            store = _store;
1183            storageKey = _storageKey;
1184            GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/Stage.this,
1185              Storer.this ); // no need to unregister, registry does not outlive this listener
1186            Window.addPageHideHandler( Storer.this ); // no need to unregister, "
1187              // Would rather listen for permanent destruction of the document, whether by
1188              // direct unload or later removal from the memory cache.  Mozilla [1] says
1189              // XPCOM is required for this [2], but maybe it is restricted to extensions.
1190              // In any case, a cross-browser solution would appear to be difficult.
1191              //
1192              // [1] https://developer.mozilla.org/En/Working_with_BFCache
1193              // [2] https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIObserver
1194              //     https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsISupportsPRUint64
1195              //     Maybe also see: https://developer.mozilla.org/en/Observer_Notifications
1196        }
1197
1198
1199        boolean isChanged; // since last sync of state/store by Storer or Restorer
1200
1201
1202        private final String storageKey;
1203
1204
1205        private final Storage store;
1206
1207
1208       // - P a g e - H i d e - H a n d l e r --------------------------------------------
1209
1210
1211        public void onPageHide( PageHideEvent _e )
1212        {
1213            if( !isChanged ) return;
1214
1215            store.setItem( storageKey, Stage.this.toString() );
1216            isChanged = false;
1217        }
1218
1219
1220       // - P r o p e r t y - C h a n g e - H a n d l e r --------------------------------
1221
1222
1223        public void onPropertyChange( PropertyChange _e ) { isChanged = true; }
1224
1225
1226    }
1227
1228
1229}