001package votorola.s.gwt.scene; // Copyright 2010-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.logical.shared.*; 006import com.google.gwt.regexp.shared.*; 007import com.google.gwt.user.client.*; 008import com.google.gwt.view.client.*; 009import com.google.web.bindery.event.shared.*; 010import votorola.a.diff.*; 011import votorola.a.web.gwt.*; 012import votorola.g.hold.*; 013import votorola.g.lang.*; 014import votorola.g.web.gwt.*; 015import votorola.g.web.gwt.event.*; 016import votorola.s.gwt.scene.feed.*; 017import votorola.s.gwt.stage.*; 018 019 020/** Scenes for Crossforum Theatre. The navigable state of the UI is specified by switches 021 * (fragment parameters) in the {@linkplain #history() history stack}. Navigation 022 * includes structural modifications to the UI such as major component changes, or true 023 * navigation such as zoom and pan. It does not include decorative changes such as those 024 * caused by track selections in the {@linkplain votorola.s.gwt.stage.StageV surrounding 025 * stage}. The switches currently reserved are: 026 * 027 * <table class='definition' style='margin-left:1em'> 028 * <tr> 029 * <th class='key'>Switch</th> 030 * <th>Controlled state</th> 031 * </tr> 032 * <tr><td class='key'>{@linkplain votorola.s.gwt.scene.vote.Votespace#aSacSwitch() a}</td> 033 * 034 * <td>Superaccount selection ({@linkplain 035 * votorola.s.gwt.scene.vote.Votespace#aSacSwitch() deprecated}).</td> 036 * 037 * </tr> 038 * <tr><td class='key'>{@linkplain #cCompositionSwitch() c}</td> 039 * 040 * <td>Composition of the theatre UI.</td> 041 * 042 * </tr> 043 * <tr><td class='key'>{@linkplain #sScopingSwitch() s}</td> 044 * 045 * <td>Scoping of the scene and other scope dependent views such as the feed.</td> 046 * 047 * </tr> 048 * </table> 049 */ 050public final class Scenes 051{ 052 053 054 /** Does nothing itself but the call forces static initialization of this class. 055 */ 056 public static void forceInitClass() {} 057 058 059 060 /** Creates the single instance of Scenes. 061 * 062 * @throws IllegalStateException if an instance of Scenes was already 063 * constructed. 064 */ 065 Scenes() { new BiteStager(); } // called by SceneIn 066 067 068 069 /** The single instance of Scenes. 070 */ 071 public static Scenes i() { return instance; } 072 073 074 private static Scenes instance; 075 076 { 077 if( instance != null ) throw new IllegalStateException(); 078 079 instance = Scenes.this; 080 } 081 082 083 // ------------------------------------------------------------------------------------ 084 085 086 /** The selection model for individual bites. Note that deselection is not supported 087 * (GWT 2.1). Instead try <code>setSelected(BiteJS.{@linkplain BiteJS#EMPTY_BITE 088 * EMPTY_BITE},true)</code>. 089 * 090 * @see <a href='http://groups.google.com/group/google-web-toolkit/browse_thread/thread/48d5b01ff8d2d7cf' 091 * >Deselection</a> 092 */ 093 public SingleSelectionModel<BiteJS> biteSelection() { return biteSelection; } 094 095 // The deselection limitation is unclear from the above notes because the cited 096 // thread concerns deselection in the CellList view (not supported), wheras the 097 // workaround I offered concerns deselection in the model. What was I thinking? 098 // If the model itself has this limitation, then it would be trivial to roll our 099 // own implementation. Just grep the code for an example. - MCA 100 101 private final SingleSelectionModel<BiteJS> biteSelection 102 = new SingleSelectionModel<BiteJS>(); 103 104 105 106 /** The URL history stacker. To keep the URLs short, only gross structural changes 107 * ought to be encoded in bookmarkable form and stacked in history, not the merely 108 * "decorative" changes such as item selection. 109 * 110 * <p>Decorative state cannot be revisted through the history controls but neither 111 * can it be destroyed. A user's attempt to navigate a series of decorative changes 112 * by going backward in history will fail, resulting instead in a structural change. 113 * Going forward again will restore both the structure and the decorations, however, 114 * because decorative state is persistent as a rule.</p> 115 */ 116 public HistoryX history() { return history; } 117 118 119 private final HistoryX history = new HistoryX( /*isFragmentShared*/false, GWTX.i().bus() ); 120 121 122 123 /** The 'c' composition switch. It directly controls the composition of the theatre 124 * UI. It consists of two short mnemonics: the first for the {@linkplain 125 * votorola.s.gwt.scene.feed.Feed.SwitchMnemonic feed} and the second for the {@linkplain 126 * Scene.SwitchMnemonic scene}. For example, <code>c=DG</code> specifies a diff feed 127 * and a geomap. 128 * 129 * <p>If no value is specified, or if it is cleared at runtime, then the window 130 * location is {@linkplain HistoryX#replace() replaced} by a new URL with a 131 * switch specifing a default composition.</p> 132 */ 133 Switch cCompositionSwitch() { return cCompositionSwitch; } 134 135 136 private final Switch cCompositionSwitch = new CSwitch(); 137 138 139 140 /** The scene that is currently shown. The value is bound via the {@linkplain 141 * GWTX#bus() event bus} to property name <tt>scene</tt>. It is never null, but it 142 * may briefly be set to {@linkplain Scene0#i() Scene0} during component transitions. 143 */ 144 public Scene scene() { return scene; } 145 146 147 private Scene scene = Scene0.i(); 148 149 150 /** Clears the scene that is currently shown by setting it to {@linkplain Scene0#i() 151 * Scene0}. 152 */ 153 void clearMap() { setScene( Scene0.i() ); } 154 155 156 /** Sets the scene that is currently shown. 157 */ 158 void setScene( final Scene newScene ) 159 { 160 if( newScene.equals( scene )) return; // throws NullPointerException if newScene null 161 162 scene = newScene; 163 fireEvent( new PropertyChange( "scene" )); 164 } 165 166 167 168 /** The 's' scoping switch. The format of the switch value depends on the particular 169 * scene shown. The value ultimately controls the scoping of the scene and other 170 * scope dependent views such as the feed. Scoping works through the intermediation 171 * of scoping models. Clients may register with those models either {@linkplain 172 * Scoping#addHandler(ScopeChangeHandler) directly} or {@linkplain 173 * ScopeChangeEvent#addHandler(ScopeChangeHandler) indirectly} to receive their 174 * events. 175 */ 176 public Switch sScopingSwitch() { return sScopingSwitch; } 177 178 179 private final Switch sScopingSwitch = new Switch( "s", history ); 180 181 182 183 /** Whether to show views and controls for resource accounting that are unfinished or 184 * deprecated. 185 * 186 * @see #setUseRAC(boolean) 187 */ 188 public static @GWTConfigCallback boolean toUseRAC() { return toUseRAC; } 189 190 191 private static boolean toUseRAC; 192 193 194 private static native void exposeUseRAC() 195 /*-{ 196 $wnd.s_gwt_scene_Scenes_toUseRAC = $entry( @votorola.s.gwt.scene.Scenes::toUseRAC() ); 197 $wnd.s_gwt_scene_Scenes_setUseRAC = $entry( @votorola.s.gwt.scene.Scenes::setUseRAC(Z) ); 198 }-*/; 199 200 static 201 { 202 assert SceneIn.isForcedInit(): "forced init " + Scenes.class.getName(); 203 exposeUseRAC(); 204 } 205 206 207 /** Sets whether to show views and controls for resource accounting that are 208 * unfinished or deprecated. This configuration method is for developers. Call 209 * it from the global configuration function {@linkplain SceneIn 210 * voGWTConfig.s_gwt_scene} like this for example:<pre 211 * 212 *> s_gwt_scene_Scenes_setUseRAC( true ); // default is false</pre> 213 * 214 * <p>You may also want to set the flag CountWAP.toSimulate, and recompile.</p> 215 * 216 * <p>This is now broken. See the comments in the constructor of 217 * SacSelectionV.SacVCell.</p> 218 * 219 * @see #toUseRAC() 220 */ 221 @Warning("broken") 222 public static @GWTConfigCallback void setUseRAC( boolean _to ) { toUseRAC = _to; } 223 224 225 226//// P r i v a t e /////////////////////////////////////////////////////////////////////// 227 228 229 private void fireEvent( final com.google.gwt.event.shared.GwtEvent<?> e ) 230 { 231 GWTX.i().bus().fireEventFromSource( e, Scenes.this ); 232 } 233 234 235 236 // ==================================================================================== 237 238 239 private final class BiteStager implements SelectionChangeEvent.Handler, TheatreInitializer 240 { 241 242 BiteStager() { Stage.i().addInitializer( BiteStager.this ); } // auto-removed 243 244 245 private void init() 246 { 247 biteSelection.addSelectionChangeHandler( BiteStager.this ); // no need to unregister, registry does not outlive this listener 248 restage(); // init state 249 } 250 251 252 // -------------------------------------------------------------------------------- 253 254 255 private void restage() 256 { 257 final DiffLookJS diff; 258 final Message message; 259 final BiteJS bite = biteSelection.getSelectedObject(); 260 if( bite == null ) 261 { 262 diff = null; 263 message = null; 264 } 265 else 266 { 267 diff = bite.difference(); 268 message = bite.message(); 269 } 270 final Stage stage = Stage.i(); 271 stage.setDifference( diff ); 272 stage.setMessage( message ); 273 } 274 275 276 // - S e l e c t i o n - C h a n g e - E v e n t . H a n d l e r ------------------ 277 278 279 public void onSelectionChange( SelectionChangeEvent _e ) { restage(); } 280 281 282 // - T h e a t r e - I n i t i a l i z e r ---------------------------------------- 283 284 285 public void initFrom( Stage _s, boolean _isReferencePending ) { init(); } 286 // Does the reverse (init to) because the feeds are sunsetting and it's not 287 // worth the effort to modify them extensively. The best thing to do here is to 288 // read the state of the stage (difference and message) and select the matching 289 // feed bite. The next best (what's done here) is the reverse: initialize the 290 // stage based on feed state, thus preventing a discontinuity between stage and 291 // feed views that might confuse the user. 292 293 294 public void initFromComplete( Stage _stage, boolean _isReferencePending ) {} 295 296 297 public void initTo( Stage _s ) { init(); } 298 299 300 public void initTo( Stage _s, TheatrePage _referrer ) { init(); } 301 302 303 public void initToComplete( Stage _s, boolean _isReferencePending ) {} 304 305 306 public void initUltimately( Stage _s, TheatrePage _referrer ) {} 307 308 } 309 310 311 312 // ==================================================================================== 313 314 315 private static final class CSwitch extends Switch implements Scheduler.ScheduledCommand 316 { 317 318 CSwitch() 319 { 320 super( "c", i().history() ); 321 spool.add( new Hold() 322 { 323 final HandlerRegistration hR = history().addPreviewHandler( 324 new ValueChangeHandler<String>() 325 { 326 // Begin recomposition in the preview dispatch where old components 327 // can be released before they learn of value changes that might break 328 // them. Scope handlers in particular are brittle. This cannot be 329 // done in the regular dispatch, regardless of how early, because the 330 // release of handlers has no effect once a dispatch is in progress. 331 public final void onValueChange( final ValueChangeEvent<String> e ) 332 { 333 final String oldValue = lastValue; // courtesy Switch.syncFromHistory() 334 final String newValue = history().getSwitchValue( name() ); 335 if( ObjectX.nullEquals( newValue, oldValue )) return; // no actual change 336 337 recompose( newValue, e ); 338 } 339 }); 340 public void release() { hR.removeHandler(); } 341 }); 342 Scheduler.get().scheduleFinally( new Scheduler.ScheduledCommand() // after ScenesV constructed 343 { 344 public void execute() { recompose( get(), null ); } // init state 345 }); 346 } 347 348 349 private static final RegExp C_PATTERN = RegExp.compile( "^([A-Z][^A-Z]*)([A-Z][^A-Z]*)$" ); 350 351 352 private void recompose( final String newValue, final ValueChangeEvent<String> e ) 353 { 354 if( newValue == null || newValue.length() == 0 ) 355 { 356 set( "DG" ); // default composition 357 history().replace(); 358 return; // replace fires a change event, so this method will re-run 359 } 360 361 final MatchResult m = C_PATTERN.exec( newValue ); 362 if( m == null ) 363 { 364 Window.alert( "Malformed composition switch: c=" + newValue ); 365 return; 366 } 367 368 Feed.SwitchMnemonic feedSMNew = Feed.SwitchMnemonic.Dum; // till proven otherwise 369 Scene.SwitchMnemonic sceneSMNew = Scene.SwitchMnemonic.Dum; 370 371 String mnemonicString; 372 mnemonicString = m.getGroup( 1 ); 373 try 374 { 375 feedSMNew = Feed.SwitchMnemonic.valueOf( mnemonicString ); 376 } 377 catch( IllegalArgumentException x ) 378 { 379 Window.alert( "Malformed feed mnemonic '" + mnemonicString 380 + "' in composition switch: c=" + newValue ); 381 } 382 383 mnemonicString = m.getGroup( 2 ); 384 try 385 { 386 sceneSMNew = Scene.SwitchMnemonic.valueOf( mnemonicString ); 387 } 388 catch( IllegalArgumentException x ) 389 { 390 Window.alert( "Malformed scene mnemonic '" + mnemonicString 391 + "' in composition switch: c=" + newValue ); 392 } 393 394 feedChanging = feedSM != feedSMNew; 395 if( feedChanging ) 396 { 397 feedSMHold.release(); 398 feedSM = feedSMNew; 399 } 400 401 mapChanging = sceneSM != sceneSMNew; 402 if( mapChanging ) 403 { 404 sceneSMHold.release(); 405 sceneSM = sceneSMNew; 406 } 407 408 Document.get().setTitle( "Crossforum Theatre " + feedSM.name() + sceneSM.name() ); 409 if( e == null ) execute(); // not a preview dispatch, rather init, so no need to wait 410 else Scheduler.get().scheduleFinally( CSwitch.this ); // continues in execute() 411 // This effectively puts the constructive part of the recomposition after 412 // the regular dispatch, thus saving the newly emplaced components from 413 // being exposed to transitional state and a flurry of accompanying events. 414 } 415 416 417 private boolean feedChanging; 418 419 private Feed.SwitchMnemonic feedSM = null; 420 421 private Hold feedSMHold = Hold0.i(); 422 423 424 private boolean mapChanging; 425 426 private Scene.SwitchMnemonic sceneSM = null; 427 428 private Hold sceneSMHold = feedSMHold; 429 430 431 // - S c h e d u l e r . S c h e d u l e d - C o m m a n d ------------------------ 432 433 434 public void execute() // continuing from compose() 435 { 436 if( feedChanging ) feedSMHold = feedSM.emplace(); 437 438 if( mapChanging ) sceneSMHold = sceneSM.emplace(); 439 } 440 } 441 442 443 444}