001package votorola.s.gwt.stage.vote; // 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.event.shared.GwtEvent;
005import com.google.gwt.event.shared.HasHandlers;
006import com.google.web.bindery.event.shared.HandlerRegistration;
007import votorola.a.count.gwt.*;
008import votorola.g.hold.*;
009import votorola.g.lang.*;
010import votorola.g.util.Holder;
011import votorola.g.web.gwt.*;
012import votorola.g.web.gwt.event.*;
013import votorola.s.gwt.stage.*;
014
015
016/** A area of <a href='../../../../../../../d/theory.xht#medium'>votespace</a> centered on
017  * one person ({@linkplain #anchor() the anchor}) and his/her direct {@linkplain
018  * votorola.a.count.XCastRelation cast relations}.  All changes to track state are
019  * signalled by {@linkplain Change change events} on the {@linkplain GWTX#bus() bus}.
020  * Event bursts are coalesced such that a single event is fired regardless of the number
021  * of state variables involved.
022  *
023  *     @see VoteTrackV
024  */
025public final class VoteTrack implements HasHandlers, Hold, Track
026{
027
028
029    /** Constructs a VoteTrack.  Call {@linkplain #release release}() when done with it.
030      */
031    public VoteTrack()
032    {
033        Stage.i().addInitializer( new TheatreInitializer() // auto-removed
034        {
035            // cf. s.gwt.stage.poll.Polltrack
036
037          // Early referrer resolution.
038          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
039            public void initTo( Stage _s ) {}
040         // public void initTo( final Stage s, final TheatrePage r ) { initR( s, r ); }
041         /// but let init complete for sake of initR guards
042            public void initTo( final Stage s, final TheatrePage r ) { initToReferrer = r; }
043            private TheatrePage initToReferrer;
044            public void initToComplete( final Stage s, final boolean rPending )
045            {
046                initComplete( s, rPending );
047                final TheatrePage r = initToReferrer;
048                if( r == null ) return; // no referrer state to transfer
049
050                assert !rPending;
051                initR( s, r );
052            }
053
054          // Late referrer resolution.
055          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
056            public void initFrom( Stage _s, boolean _rPending ) {}
057            public void initFromComplete( final Stage s, final boolean rPending )
058            {
059                initComplete( s, rPending );
060                if( rPending ) initFromActorName = s.getActorName(); // continues at initUltimately
061            }
062            private String initFromActorName; // as restored from single page persistence
063            public void initUltimately( final Stage s, final TheatrePage r )
064            {
065                if( r == null ) return; // no referrer state to transfer
066
067                if( !ObjectX.nullEquals( s.getActorName(), initFromActorName )) return;
068                  // changed by user or prop, do not clobber
069
070                initR( s, r );
071            }
072
073          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
074            public void initComplete( final Stage s, final boolean rPending )
075            {
076                initWaitSpool.unwind();
077                new Tracker( s );
078            }
079            private void initR( final Stage s, final TheatrePage r )
080            {
081                if( r == null ) return; // no referrer state to transfer
082
083                final String rActorName = r.getActorName();
084                if( rActorName == null ) return; // no actor on referrer
085
086                final boolean willBePolless = s.getPollName() == null && r.getPollName() == null;
087                  // will be, that is, after prop such as polltrack carries it across
088                if( s.getDefaultActorName() != null && willBePolless ) return;
089                  // This is actor's page because default actor staged.  Don't carry an
090                  // actor (like a different actor) here when no poll will be staged.
091                  // Vote track is useless without poll and probably has no useful
092                  // information to preserve.  (The specific motivation here is to avoid
093                  // odd behaviour of actor mysteriously carried to another's home page.)
094
095                final JavaScriptObject voc = WindowX.js()._get( "voc" );
096                if( voc != null )
097                {
098                    final String pageJClass = voc._getString( "pageJClass" );
099                      // per a.web.wic.VPageHTML
100                    if( "votorola.s.wic.count.WP_Votespace".equals(pageJClass)
101                     || "votorola.s.wic.count.WP_Rank".equals(pageJClass) ) return;
102                      // per s.gwt.wic.CountIn.  These have PositionPager, cannot tolerate
103                      // actor init other than own
104                }
105
106                Stage.setActorName( rActorName );
107            }
108        });
109    }
110
111
112
113    /** Returns the {@linkplain Stage#tracks() staged instance} of VoteTrack, or null if
114      * none is staged.
115      */
116    public static VoteTrack i( final Stage stage )
117    {
118        for( Track t: stage.tracks() ) if( t instanceof VoteTrack ) return (VoteTrack)t;
119
120        return null;
121    }
122
123
124
125    /** A spool that unwinds just before this track commences initializing, which it does
126      * on "{@linkplain TheatreInitializerC#initComplete(Stage, boolean) init complete}".
127      * Initializers that need to run after the stage is ready and before the first event
128      * is fired from this track should wrap themselves here.
129      */
130    Spool initWaitSpool() { return initWaitSpool; }
131
132
133        private final Spool initWaitSpool = new Spool1();
134
135
136
137   // ------------------------------------------------------------------------------------
138
139
140    /** The count node of the default or current actor complete with {@linkplain
141      * CountNodeJS#voters() voters}, or null if either no anchor is {@linkplain
142      * #anchorName(Stage) expected}, or the node is yet unknown.  The anchor may either
143      * be {@linkplain #toPin(Stage) pinned} or left free to move in votespace.
144      */
145    public CountNodeJS anchor() { return anchor; }
146
147
148        private CountNodeJS anchor;
149
150
151        /** A constant holder for the anchor node.
152          */
153        Holder<CountNodeJS> anchorHolder() { return anchorHolder; }
154
155
156            private final Holder<CountNodeJS> anchorHolder = new Holder<CountNodeJS>()
157            {
158                public CountNodeJS get() { return anchor; }
159            };
160
161
162        private void retrackAnchor( final Stage stage ) // only called when count not null
163        {
164            final String anchorName = anchorName( stage );
165            if( anchorName == null ) clearAnchor();
166            else if( anchor != null && anchor.count() == count
167              && anchor.name().equals(anchorName) ) return;
168            else // new anchor
169            {
170                final CountJS.NodeRequestRecord record = count.nodeRequestRecord( anchorName );
171                final CountNodeJS anchorNew;
172                if( CountJS.isNodeRequestComplete( record )) anchorNew = record.node();
173                else // not fully requested
174                {
175                    final String loc = count.requestNode_loc( anchorName,
176                      GWTX.stringBuilderClear() ).toString();
177                    final AtomicAsyncCallback<CountNodeJS, CountCache.WAPResponse> callbackA =
178                      AtomicAsyncCallback.wrap( exchangeAtomizer, new CountCallback<CountNodeJS>(
179                        loc, stage )
180                    {
181                        public void onSuccess( final CountNodeJS aNew ) {
182                        forceAnchor( aNew ); }
183                        // no need to clear on failure, already cleared below on request
184                    });
185                    callbackA.init( count.requestNode( loc, anchorName,
186                      SpooledAsyncCallback.wrap(spool,callbackA) ));
187                    anchorNew = null;
188                }
189                if( anchorNew == null ) clearAnchor( record );
190                else forceAnchor( anchorNew );
191            }
192        }
193
194
195        private void clearAnchor() { clearAnchor( null ); } // and dependents
196
197
198        private void clearAnchor( final CountJS.NodeRequestRecord record ) // and maybe dependents
199        {
200            String expectedCandidateName = null;
201            if( record != null )
202            {
203                final CountNodeJS newAnchor = record.node();
204                if( newAnchor != null ) expectedCandidateName = newAnchor.candidateName();
205                  // anchor being cleared until new anchor's voter's are fetched
206            }
207            if( anchor != null )
208            {
209                anchor = null;
210                gun.schedule();
211            }
212            clearNonMatchingCandidate( expectedCandidateName );
213              // do not clear candidate unecessarily, as it will disable peers and cause
214              // peers view to disappear and reappear for no good reason
215        }
216
217
218        private void forceAnchor( final CountNodeJS anchorNew ) // unguarded setter
219        {
220            assert anchorNew != null;
221            anchor = anchorNew;
222            gun.schedule();
223            retrackCandidate();
224        }
225
226
227
228    /** Returns the mailish username of the person who should be {@linkplain #anchor()
229      * anchor}, or null if nobody should be.
230      */
231    static String anchorName( final Stage stage )
232    {
233        String name = stage.getDefaultActorName();
234        if( name == null ) name = stage.getActorName();
235        return name;
236    }
237
238
239
240    /** The count node of the {@linkplain #anchor() anchor}'s candidate complete with
241      * {@linkplain CountNodeJS#voters() voters}, or null if either the anchor has no
242      * candidate, or the node is yet unknown.
243      */
244    public CountNodeJS candidate() { return candidate; }
245
246
247        private CountNodeJS candidate;
248
249
250        /** A constant holder for the variable candidate.
251          */
252        Holder<CountNodeJS> candidateHolder() { return candidateHolder; }
253
254
255            private final Holder<CountNodeJS> candidateHolder = new Holder<CountNodeJS>()
256            {
257                public CountNodeJS get() { return candidate; }
258            };
259
260
261        private void retrackCandidate() // only called when anchor and count not null
262        {
263            if( !anchor.isVoter() )
264            {
265                clearCandidate();
266                return;
267            }
268
269            final String candidateName = anchor.candidateName();
270            if( candidate != null && candidate.count() == count
271              && candidate.name().equals(candidateName) ) return;
272            else // new candidate
273            {
274                final CountJS.NodeRequestRecord record = count.nodeRequestRecord( candidateName );
275                final CountNodeJS candidateNew;
276                if( CountJS.isNodeRequestComplete( record )) candidateNew = record.node();
277                else // not fully requested
278                {
279                    final String loc = count.requestNode_loc( candidateName,
280                      GWTX.stringBuilderClear() ).toString();
281                    final AtomicAsyncCallback<CountNodeJS, CountCache.WAPResponse> callbackA =
282                      AtomicAsyncCallback.wrap( exchangeAtomizer, new CountCallback<CountNodeJS>(
283                        loc, Stage.i() )
284                    {
285                        public void onSuccess( final CountNodeJS cNew ) { forceCandidate( cNew ); }
286                        // no need to clear on failure, already cleared below on request
287                    });
288                    callbackA.init( count.requestNode( loc, candidateName,
289                      SpooledAsyncCallback.wrap(spool,callbackA) ));
290                    candidateNew = null;
291                }
292                if( candidateNew == null ) clearCandidate();
293                else forceCandidate( candidateNew );
294            }
295        }
296
297
298        private void clearCandidate() // and dependents
299        {
300            if( candidate != null )
301            {
302                candidate = null;
303                hasTightCycle = false;
304                gun.schedule();
305            }
306            clearDownstream();
307        }
308
309
310        private void clearNonMatchingCandidate( final String name ) // or null
311        {
312            if( candidate == null ) return;
313
314            if( name == null || !name.equals( candidate.name() )) clearCandidate();
315        }
316
317
318        private void forceCandidate( final CountNodeJS candidateNew ) // unguarded setter
319        {
320            assert candidateNew != null;
321            candidate = candidateNew;
322            hasTightCycle = anchor.name().equals(candidate.candidateName()) && candidate.isVoter();
323            gun.schedule();
324            retrackDownstream();
325        }
326
327
328
329    /** The tracked count, or null if it is yet unknown.
330      */
331    public CountJS count() { return count; }
332
333
334        private CountJS count;
335
336
337        private void retrackCount( final Stage stage ) // and everything else
338        {
339            final String pollName = stage.getPollName();
340            if( pollName == null ) clearCount();
341            else if( count != null && count.pollName().equals( pollName )) retrackAnchor( stage );
342            else // new poll
343            {
344                final CountCache countCache = CountCache.i();
345                final CountCache.RequestRecord record = countCache.requestRecord( pollName );
346                final CountJS countNew;
347                if( record == null ) // not previously requested
348                {
349                    final String loc = CountCache.requestCount_loc( pollName,
350                      GWTX.stringBuilderClear() ).toString();
351                    final AtomicAsyncCallback<CountJS, CountCache.WAPResponse> callbackA =
352                      AtomicAsyncCallback.wrap( exchangeAtomizer, new CountCallback<CountJS>(
353                        loc, stage )
354                    {
355                        public void onSuccess( final CountJS cNew ) { forceCount( cNew ); }
356                        // no need to clear on failure, already cleared below on request
357                    });
358                    callbackA.init( countCache.requestCount( pollName, loc,
359                      SpooledAsyncCallback.wrap(spool,callbackA) ));
360                    countNew = null;
361                }
362                else countNew = record.count();
363                if( countNew == null ) clearCount();
364                else forceCount( countNew );
365            }
366        }
367
368
369        private void clearCount()
370        {
371            if( count == null )
372            {
373                assert anchor == null && candidate == null;
374                return;
375            }
376
377            count = null;
378            gun.schedule();
379            clearAnchor();
380        }
381
382
383        private void forceCount( final CountJS countNew ) // unguarded setter
384        {
385            assert countNew != null;
386            count = countNew;
387            gun.schedule();
388            retrackAnchor( Stage.i() );
389        }
390
391
392
393    /** The count node of the {@linkplain #candidate() candidate}'s candidate complete
394      * with {@linkplain CountNodeJS#voters() voters}, or null if either the candidate has
395      * no candidate, or the node is yet unknown.
396      */
397    public CountNodeJS downstream() { return downstream; }
398
399
400        private CountNodeJS downstream;
401
402
403        /** A constant holder for the variable downstream node.
404          */
405        Holder<CountNodeJS> downstreamHolder() { return downstreamHolder; }
406
407
408            private final Holder<CountNodeJS> downstreamHolder = new Holder<CountNodeJS>()
409            {
410                public CountNodeJS get() { return downstream; }
411            };
412
413
414        private void retrackDownstream() // only called when anchor, count and candidate not null
415        {
416            if( !candidate.isVoter() )
417            {
418                clearDownstream();
419                return;
420            }
421
422            final String downstreamName = candidate.candidateName();
423            if( downstream != null && downstream.count() == count
424              && downstream.name().equals(downstreamName) ) return;
425            else // new downstream
426            {
427                final CountJS.NodeRequestRecord record = count.nodeRequestRecord( downstreamName );
428                final CountNodeJS downstreamNew;
429                if( CountJS.isNodeRequestComplete( record )) downstreamNew = record.node();
430                else // not fully requested
431                {
432                    final String loc = count.requestNode_loc( downstreamName,
433                      GWTX.stringBuilderClear() ).toString();
434                    final AtomicAsyncCallback<CountNodeJS, CountCache.WAPResponse> callbackA =
435                      AtomicAsyncCallback.wrap( exchangeAtomizer, new CountCallback<CountNodeJS>(
436                        loc, Stage.i() )
437                    {
438                        public void onSuccess( final CountNodeJS dNew ) { forceDownstream( dNew ); }
439                        // no need to clear on failure, already cleared below on request
440                    });
441                    callbackA.init( count.requestNode( loc, downstreamName,
442                      SpooledAsyncCallback.wrap(spool,callbackA) ));
443                    downstreamNew = null;
444                }
445                if( downstreamNew == null ) clearDownstream();
446                else forceDownstream( downstreamNew );
447            }
448        }
449
450
451        private void clearDownstream()
452        {
453            if( downstream == null ) return;
454
455            downstream = null;
456            gun.schedule();
457        }
458
459
460        private void forceDownstream( final CountNodeJS downstreamNew ) // unguarded setter
461        {
462            assert downstreamNew != null;
463            downstream = downstreamNew;
464            gun.schedule();
465        }
466
467
468
469    /** True if the {@linkplain #candidate() candidate} and anchor are in a tight cycle;
470      * false if either there is no candidate, or its node is yet unknown.  When true the
471      * candidate will appear not only as the candidate, but also as a {@linkplain
472      * #voters() voter}.
473      *
474      *     @see <a href='../../../../../../../d/theory.xht#cycle' target='_top'>tight cycle</a>
475      */      // per INLDOC
476    public boolean hasTightCycle() { return hasTightCycle; }
477
478
479        private boolean hasTightCycle;
480
481
482
483    /** The board of peers, meaning either the anchor node's {@linkplain
484      * votorola.a.count.XCastRelation#CO_VOTER co-voters} or the {@linkplain
485      * votorola.a.count.XCastRelation#CO_BASE base co-candidates}.  The state of the
486      * board is maintained according to the following conditions applied in order:
487      *
488      * <table class='simple' style='margin:1em'>
489      *     <tr><th></th>
490      *         <th>Condition</th>
491      *         <th>Content</th>
492      *         </tr>
493      *     <tr><td>(a)</td>
494      *         <td>no count</td>
495      *         <td>{@linkplain Board#isEnabled() disabled}</td>
496      *         </tr>
497      *     <tr><td>(b)</td>
498      *         <td>no anchor</td>
499      *         <td>nodes are {@linkplain CountJS#baseCandidates() base candidates}</td>
500      *         </tr>
501      *     <tr><td>(c)</td>
502      *         <td>anchor pending
503      *             but named in candidate's {@linkplain CountNodeJS#voters() voters}</td>
504      *         <td>nodes are candidate's voters</td>
505      *         </tr>
506      *     <tr><td>(d)</td>
507      *         <td>anchor pending but name matches root candidate</td>
508      *         <td>nodes are base candidates</td>
509      *         </tr>
510      *     <tr><td>(e)</td>
511      *         <td>anchor pending</td>
512      *         <td>disabled</td>
513      *         </tr>
514      *     <tr><td>(f)</td>
515      *         <td>anchor is root candidate</td>
516      *         <td>nodes are base candidates</td>
517      *         </tr>
518      *     <tr><td>(g)</td>
519      *         <td>no candidate</td>
520      *         <td>nodes are base candidates,
521      *             anchor is {@linkplain Board#mosquito() mosquito} orphan</td>
522      *         </tr>
523      *     <tr><td>(h)</td>
524      *         <td>candidate pending</td>
525      *         <td>disabled</td>
526      *         </tr>
527      *     <tr><td>(i)</td>
528      *         <td style='font-style:italic'>default</td>
529      *         <td>nodes are candidate's voters</td>
530      *         </tr>
531      *     </table>
532      *
533      * <p>The {@linkplain #anchor() anchor} (if any) is always included, either as a
534      * {@linkplain PeerBoard#node(int) dart-sectored node}, or as a {@linkplain
535      * PeerBoard#mosquito() mosquito}.</p>
536      */
537    public PeerBoard peers() { return peers; }
538
539
540        private final PeerBoard peers = new PeerBoard( VoteTrack.this );
541
542
543        private void syncPeers()
544        {
545     /* a */if( count == null )
546            {
547                peers.disable();
548                return;
549            }
550
551            final String anchorName = anchorName( Stage.i() );
552            if( anchorName == null )
553            {
554     /* b */    peers.enable( null, count.baseCandidates(), /*isEndBoard*/true );
555            }
556            else if( anchor == null ) // pending
557            {
558                JsArray<CountNodeJS> coNodes;
559                if( candidate != null )
560                {
561     /* c */        coNodes = candidate.voters();
562                    if( CountNodeJS.findNode(anchorName,coNodes) != null )
563                    {
564                        peers.enable( null, coNodes, /*isEndBoard*/false );
565                        return;
566                    }
567                }
568
569     /* d */    coNodes = count.baseCandidates();
570                final CountNodeJS anchorAtEnd = CountNodeJS.findNode( anchorName, coNodes );
571                if( anchorAtEnd != null && !anchorAtEnd.isVoter() )
572                {
573                    peers.enable( null, coNodes, /*isEndBoard*/true );
574                    return;
575                }
576
577     /* e */    peers.disable();
578            }
579            else if( !anchor.isVoter() )
580            {
581     /* f */    if( anchor.directVoterCount() > 0L )
582                {
583                    peers.enable( unsectoredAnchor(), count.baseCandidates(), /*isEndBoard*/true );
584                }
585     /* g */    else peers.enable( anchor, count.baseCandidates(), /*isEndBoard*/true );
586            }
587     /* h */else if( candidate == null ) peers.disable();
588     /* i */else peers.enable( unsectoredAnchor(), candidate.voters(), /*isEndBoard*/false );
589        }
590
591
592
593    /** Answers whether the {@linkplain #anchor() anchor} should currently be pinned to a
594      * single node in votespace, or free to follow the {@linkplain Stage#getActorName()
595      * staged actor}.  It should be pinned when a {@linkplain Stage#getDefaultActorName()
596      * default actor} is set, otherwise it should be free.
597      */
598    static boolean toPin( final Stage stage ) { return stage.getDefaultActorName() != null; }
599
600
601
602    /** The board of the anchor node's direct voters.  If there is no anchor node, then
603      * this board is disabled.  If the track has a {@linkplain #hasTightCycle tight
604      * cycle}, then the candidate is included as a {@linkplain Board#node(int) sectored
605      * node} or {@linkplain Board#mosquito() mosquito}.
606      */
607    public Board voters() { return voters; }
608
609
610        private final Board voters = new Board( VoteTrack.this );
611
612
613        private void syncVoters()
614        {
615            if( anchor == null ) voters.disable();
616            else
617            {
618                CountNodeJS mosquito = hasTightCycle && candidate.dartSector() == 0? candidate: null;
619                voters.enable( mosquito, anchor.voters(), /*isEndBoard*/false );
620            }
621        }
622
623
624
625   // - H a s - H a n d l e r s ----------------------------------------------------------
626
627
628    public void fireEvent( final GwtEvent<?> e )
629    {
630        assert e instanceof Change;
631        syncPeers();
632        syncVoters();
633        GWTX.i().bus().fireEventFromSource( e, VoteTrack.this );
634    }
635
636
637
638   // - H o l d --------------------------------------------------------------------------
639
640
641    public void release() { spool.unwind(); }
642
643
644
645   // - T r a c k ------------------------------------------------------------------------
646
647
648    public VoteTrackV newView( StageV _stageV )
649    {
650        return new VoteTrackV( VoteTrack.this, /*isBottomFixed*/false );
651    }
652
653
654
655//// P r i v a t e ///////////////////////////////////////////////////////////////////////
656
657
658    private final JSONPAtomizer<CountCache.WAPResponse> exchangeAtomizer =
659      new JSONPAtomizer<CountCache.WAPResponse>();
660
661
662
663    private final Change.CoalescingGun gun = new Change.CoalescingGun( /*source*/VoteTrack.this,
664      CoalescingSchedulerS.DEFERRED ); // VoteTrackV.Animator depends on delay
665
666
667
668    private final Spool spool = new Spool1();
669
670
671
672    private CountNodeJS unsectoredAnchor() { return anchor.dartSector() == 0? anchor: null; }
673
674
675
676   // ====================================================================================
677
678
679    private final class Tracker implements PropertyChangeHandler, Scheduler.ScheduledCommand
680    {
681        // cf. s.gwt.scene.vote.Votespace.Scoper
682
683        Tracker( final Stage stage )
684        {
685            spool.add( new Hold()
686            {
687                final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
688                  PropertyChange.TYPE, /*source*/stage, Tracker.this );
689                public void release() { hR.removeHandler(); }
690            });
691            retrackCount( stage ); // init
692        }
693
694
695        private final CoalescingSchedulerS coalescer = new CoalescingSchedulerS(
696          CoalescingSchedulerS.FINALLY, Tracker.this );
697
698
699       // - P r o p e r t y - C h a n g e - H a n d l e r --------------------------------
700
701
702        public final void onPropertyChange( final PropertyChange e )
703        {
704            if( spool.isUnwinding() ) return;
705
706            final String name = e.propertyName();
707            if( name.equals("pollName") || name.equals("actorName") ) coalescer.schedule();
708              // which later calls retrackCount
709        }
710
711
712       // - 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 ------------------------
713
714
715        public final void execute() { retrackCount( Stage.i() ); }
716
717    }
718
719
720
721}