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}