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}