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}