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.event.dom.client.ClickEvent; 004import com.google.gwt.event.dom.client.ClickHandler; 005import com.google.gwt.event.dom.client.MouseOutEvent; 006import com.google.gwt.event.dom.client.MouseOutHandler; 007import com.google.gwt.event.dom.client.MouseOverEvent; 008import com.google.gwt.event.dom.client.MouseOverHandler; 009import com.google.gwt.event.shared.GwtEvent; 010import com.google.web.bindery.event.shared.HandlerRegistration; 011import org.vectomatic.dom.svg.*; 012import votorola.a.count.*; 013import votorola.a.count.gwt.*; 014import votorola.a.web.gwt.*; 015import votorola.g.hold.*; 016import votorola.g.lang.*; 017import votorola.g.web.gwt.*; 018import votorola.g.web.gwt.event.*; 019import votorola.g.web.gwt.svg.*; 020import votorola.s.gwt.stage.*; 021import votorola.s.gwt.stage.light.*; 022 023import static com.google.gwt.dom.client.Style.Display.INLINE; 024import static votorola.a.count.XCastRelation.VOTER; 025import static votorola.a.count.XCastRelation.CO_VOTER; 026import static votorola.a.count.XCastRelation.CANDIDATE; 027 028 029/** A view of a count node in the shape of an arrow segment. Clicking on the view sets or 030 * unsets the node as the {@linkplain Stage#getActorName() stage actor}.<pre> 031 * 032 * protrusion 033 * / 034 * |---- length ---|--| 035 * 036 * p0 +---------------+ - 037 * \ \ | half thickness 038 * + * + - 039 * / / 040 * +---------------+</pre> 041 * 042 * If the node is a <a href='../../../../../../../d/theory.xht#cycle' 043 * target='_top'>cycler</a>, then a spot (*) is visible in the middle of the view. 044 * 045 * @see <a href='VoteTrackV.html#ackC'>Acknowledgement to Christian Weilbach</a> 046 */ 047public class NodeV extends OMSVGGElement 048{ 049 050 051 /** Constructs a NodeV. 052 * 053 * @see #box() 054 * @see #dartSector() 055 */ 056 NodeV( Box _box, int _dartSector, final VoteTrack track ) 057 { 058 // track needed only because box.trackV is null during init 059 box = _box; 060 dartSector = _dartSector; 061 062 // View. 063 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 064 addClassNameBaseVal( "CountNode" ); 065 final XCastRelation place = box.place(); 066 String s = Character.toString( place.symbol() ); 067 if( place == XCastRelation.VOTER || place == XCastRelation.CO_VOTER ) s += dartSector; 068 dartLightClassName = s; 069 addClassNameBaseVal( dartLightClassName ); 070 appendChild( new ArrowSegment() ); 071 appendChild( new CycleSpot() ); 072 073 // Controllers. 074 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 075 new Clicker(); 076 highlighter = new Highlighter(); 077 new Mouser(); 078 sensor = new Sensor(); 079 tracker = new Tracker( track ); 080 } 081 082 083 084 /** Forces static initialization of this class and appends a post-configuration 085 * initializer to the specified spool. 086 * 087 * @param configurationSpool a spool that will unwind immediately after 088 * {@linkplain StageMod module configuration} is complete. 089 */ 090 public static void forceInitClass( final Spool configurationSpool ) 091 { 092 configurationSpool.add( new Hold() 093 { 094 public void release() 095 { 096 if( deselectionGuard == null ) deselectionGuard = new DefaultDeselect(); 097 } 098 }); 099 } 100 101 102 103 // ------------------------------------------------------------------------------------ 104 105 106 /** The ancestral container of this view, the {@linkplain MajorV#spool() spool} of 107 * which controls its life cycle. 108 */ 109 public final Box box() { return box; } 110 111 112 private final Box box; 113 114 115 116 /** The CSS class name for the {@linkplain DartLight dart light}, such as "v17". 117 */ 118 final String dartLightClassName() { return dartLightClassName; } 119 120 121 private final String dartLightClassName; 122 123 124 125 /** The dart sector to which this view is restricted, or zero if it is not restricted 126 * to any particular sector. 127 * 128 * @see votorola.a.count.CountNode#dartSector() 129 */ 130 final int dartSector() { return dartSector; } 131 132 133 private final int dartSector; 134 135 136 137 /** The count node on which this view is modelled, or null if none is modelled. 138 * 139 * @see #setCountNode(CountNodeJS) 140 */ 141 public final CountNodeJS getCountNode() { return countNode; } // bearing in mind this is a subclass of OMNode 142 143 144 private CountNodeJS countNode; 145 146 147 /** Sets the count node on which this view is modelled. 148 * 149 * @return true if setting the node resulted in a change, false if that node 150 * was already set. 151 * @see #getCountNode() 152 * @throws IllegalArgumentException if the node does not have the same dart 153 * sector as this view. 154 */ 155 final boolean setCountNode( final CountNodeJS node ) 156 { 157 if( ObjectX.nullEquals( node, countNode )) return false; 158 // Guard against pointless repaint, though it is not currently expensive. 159 // This is valid only because the node is immutable except its 'voters' 160 // array, which has no bearing on the view. 161 162 final VoteTrack track = box.trackV().track(); 163 if( node == null ) 164 { 165 if( countNode != null ) removeClassNameBaseVal( "occupied" ); // it changed 166 clearCycler(); 167 sensor.clear(); 168 } 169 else 170 { 171 if( dartSector != 0 && node.dartSector() != dartSector ) 172 { 173 throw new IllegalArgumentException( "dart sector mismatch" ); 174 } 175 176 if( countNode == null ) addClassNameBaseVal( "occupied" ); // it changed 177 if( node.isCycler() ) setCycler(); 178 else clearCycler(); 179 sensor.set( track.count(), node ); 180 } 181 countNode = node; 182 tracker.run( track ); 183 highlighter.relight(); 184 return true; 185 } 186 187 188 189 /** Answers whether the node is in a tight cycle with the anchor. 190 * 191 * @see <a href='../../../../../../../d/theory.xht#cycle' target='_top'>tight cycle</a> 192 */ 193 final boolean isTightCycler() { return isTightCycler; } 194 195 196 private boolean isTightCycler; 197 198 199 200 /** The horizontal length of the top line of the drawing as currently drawn in the SVG 201 * viewport. 202 */ 203 public final float length() 204 { 205 final OMSVGPathSegLinetoRel seg = (OMSVGPathSegLinetoRel) 206 arrowSegment().getPathSegList().getItem( 1 ); 207 return seg.getX(); 208 } 209 210 211 212 /** The point at the top left of the drawing as currently drawn in the SVG viewport. 213 */ 214 public final OMSVGPathSegMovetoAbs p0() 215 { 216 return (OMSVGPathSegMovetoAbs)arrowSegment().getPathSegList().getItem( 0 ); 217 } 218 219 220 221 /** The horizontal extent of the arrowhead beyond the {@linkplain #length() length}, 222 * as currently drawn in the SVG viewport. 223 */ 224 public final float protrusion() 225 { 226 final OMSVGPathSegLinetoRel seg = (OMSVGPathSegLinetoRel) 227 arrowSegment().getPathSegList().getItem( 2 ); 228 return seg.getX(); 229 } 230 231 232 233 /** Redraws this view according to the provided parameters. The view never 234 * automatically initiates a repaint, but depends entirely on its MajorV ancestor for 235 * this. 236 */ 237 void repaint( final float x, float length, final float protrusion, final int y, 238 final float halfThickness ) 239 { 240 arrowSegment().repaint( x, length, protrusion, y, halfThickness ); 241 cycleSpot().repaint( x, length, protrusion, y, halfThickness ); 242 } 243 244 245 246 /** Sets the type of {@linkplain DeselectionGuard deselection guard}. Call this 247 * method from the global configuration function {@linkplain StageMod 248 * voGWTConfig.s_gwt_stage} in this fashion:<pre> 249 * 250 * s_gwt_stage_vote_CountNodeV_setDeselectionGuard( 'Blind' ); 251 * // default is 'Default'</pre> 252 * 253 * Or call it at any time during normal operation. 254 * 255 * @throws IllegalArgumentException if the provided guard name is unrecognized. 256 */ 257 public static @GWTConfigCallback void setDeselectionGuard( final String guardName ) 258 { 259 final DeselectionGuard g; 260 if( BlindDeselect.NAME.equals( guardName )) g = new BlindDeselect(); 261 else if( DefaultDeselect.NAME.equals( guardName )) g = new DefaultDeselect(); 262 else if( LaxDeselect.NAME.equals( guardName )) g = new LaxDeselect(); 263 else throw new IllegalArgumentException( "no such guard: " + guardName ); 264 265 deselectionGuard = g; 266 GWTX.i().bus().fireEventFromSource( new PropertyChange("deselectionGuard"), 267 NodeV.class ); 268 } 269 270 271 /** The value is bound via the {@linkplain GWTX#bus() event bus} to property name 272 * <tt>deselectionGuard</tt> on source NodeV.class. 273 */ 274 private static DeselectionGuard deselectionGuard; 275 276 277 private static native void exposeDeselectionGuard() 278 /*-{ 279 $wnd.s_gwt_stage_vote_CountNodeV_setDeselectionGuard = $entry( 280 @votorola.s.gwt.stage.vote.NodeV::setDeselectionGuard(Ljava/lang/String;) ); 281 }-*/; 282 283 284 static 285 { 286 assert StageMod.isForcedInit(): "forced init " + NodeV.class.getName(); 287 exposeDeselectionGuard(); 288 } 289 290 291 292 /** The stroke width for painting the main figure (arrow segment). 293 */ 294 public static final int STROKE_WIDTH = 2; // from stage/vote/track.css 295 296 297 298 // - O M - N o d e ------------------------------------------------------------------- 299 300 301 public @Override final void fireEvent( final GwtEvent<?> e ) 302 { 303 try{ super.fireEvent( e ); } 304 catch( Exception x ) { GWTX.handleUncaughtException( x ); } // q.v. for reason 305 } 306 307 308 309 public @Override final NodeV getNextSibling() { return (NodeV)super.getNextSibling(); } 310 // auto and explicit {@inheritDoc} fail, JDK 1.7 311 312 313 314 public @Override final NodeV getPreviousSibling() // ditto {@inheritDoc} failure 315 { 316 return (NodeV)super.getPreviousSibling(); 317 } 318 319 320 321 // ==================================================================================== 322 323 324 // /** The {@value NAME} deselection guard. It allows for deselection of nodes that are 325 // * base candidates or non-participants (both being tracked at the base), provided no 326 // * {@linkplain Stage#getDefaultActorName() default actor} is set. This allows the 327 // * track to fall cleanly into its default state where only the base peer board is 328 // * displayed. This will not normally happen when a default actor is set, of course, 329 // * and that is why deselection is disabled in that case. 330 // */ 331 // static final class BaseCandidateDeselect implements DeselectionGuard 332 // { 333 // 334 // public void guard( final NodeV nodeV ) 335 // { 336 // final CountNodeJS node = nodeV.countNode; 337 // nodeV.isClickable = Stage.i().getDefaultActorName() == null && ( node.isBaseCandidate() 338 // ||/*non-participant*/ !node.isVoter() && !node.isCandidate() ); 339 // } 340 // 341 // 342 // static final String NAME = "BaseCandidate"; 343 // 344 // } 345 ///// But the track is pinned when a default actor is set, so its unclear why deselection 346 ///// should be forbidden in that case. Being pinned, the track won't snap back far. 347 348 349 350 // ==================================================================================== 351 352 353 // /** The {@value NAME} deselection guard. It is the same as {@linkplain 354 // * BaseCandidateDeselect BaseCandidateDeselect} except it always allows for the 355 // * deselection of mosquitos. This is useful because there is ordinarily no way to 356 // * navigate away from a mosquito once it is selected except by deselecting it. 357 // */ 358 // static final class BaseMosquitoDeselect implements DeselectionGuard 359 // { 360 // 361 // public void guard( final NodeV nodeV ) 362 // { 363 // final CountNodeJS node = nodeV.countNode; 364 // nodeV.isClickable = node.dartSector() == 0 365 // || Stage.i().getDefaultActorName() == null && node.isBaseCandidate(); 366 // } 367 // 368 // 369 // static final String NAME = "BaseMosquito"; 370 // 371 // } 372 ///// But a mosquito may be deselected by selecting another node, so the use case is 373 ///// unclear. Maybe the intention was to deselect a non-participant node, because 374 ///// otherwise it may get stuck. 375 376 377 378 // ==================================================================================== 379 380 381 /** The {@value NAME} deselection guard. It allows for deselection of any node. If a 382 * {@linkplain Stage#getDefaultActorName() default actor} is set, then it cannot 383 * actually be deselected. But this guard will not prevent the attempt. 384 */ 385 static final class BlindDeselect implements DeselectionGuard 386 { 387 388 public void guard( final NodeV nodeV ) { nodeV.isClickable = true; } 389 390 391 static final String NAME = "Blind"; 392 393 } 394 395 396 397 // ==================================================================================== 398 399 400 /** A container of node views. 401 */ 402 public static abstract class Box extends MajorV 403 { 404 405 /** Creates a new Box. 406 * 407 * @see MajorV#place() 408 * @see MajorV#trackV() 409 */ 410 Box( XCastRelation _place, VoteTrackV _trackV ) { super( _place, _trackV ); } 411 412 413 // ------------------------------------------------------------------------------------ 414 415 416 /** The nodal components of this view. 417 */ 418 public abstract Iterable<NodeV> nodeViews(); 419 420 421 /** The source of {@linkplain Change change events} that are fired on the 422 * {@linkplain GWTX#bus() bus} after each repainting of the node views. 423 */ 424 public abstract Object painter(); 425 426 } 427 428 429 430 // ==================================================================================== 431 432 433 /** The {@value NAME} deselection guard. It disallows deselection of a {@linkplain 434 * Stage#getDefaultActorName() default actor} node, because such a node cannot really 435 * be deselected in any case. If the track is {@linkplain VoteTrack#toPin(Stage) 436 * unpinned}, then it also disallows the deselection of all but base candidate and 437 * non-participant nodes. Deselection will cause the unpinned track to drop to its 438 * default state in which only the base peer board is displayed. The user may easily 439 * reselect a base candidate or non-participant in that state, while other nodes 440 * might require a climb back up the tree. 441 */ 442 static final class DefaultDeselect implements DeselectionGuard 443 { 444 445 public void guard( final NodeV nodeV ) 446 { 447 final Stage stage = Stage.i(); 448 final CountNodeJS node = nodeV.countNode; 449 if( VoteTrack.toPin( stage )) 450 { 451 nodeV.isClickable = !node.name().equals( stage.getDefaultActorName() ); 452 } 453 else // unpinned track 454 { 455 assert stage.getDefaultActorName() == null; // node cannot be default actor 456 nodeV.isClickable = node.isBaseCandidate() 457 ||/*non-participant*/ !node.isVoter() && !node.isCandidate(); 458 } 459 } 460 461 462 static final String NAME = "Default"; 463 464 } 465 466 467 468 // ==================================================================================== 469 470 471 /** A controller that determines whether a selected node view may be deselected. 472 */ 473 interface DeselectionGuard 474 { 475 476 /** Sets nodeV.{@linkplain NodeV#isClickable isClickable} according to whether 477 * nodeV may be deselected. nodeV must be modelled by an actual node. 478 */ 479 public void guard( final NodeV nodeV ); 480 481 } 482 483 484 485 // ==================================================================================== 486 487 488 /** The {@value NAME} deselection guard. It allows for deselection of any node except 489 * that of the {@linkplain Stage#getDefaultActorName() default actor}, which cannot 490 * actually be deselected in any case. 491 */ 492 static final class LaxDeselect implements DeselectionGuard 493 { 494 495 public void guard( final NodeV nodeV ) 496 { 497 nodeV.isClickable = !nodeV.countNode.name().equals( Stage.i().getDefaultActorName() ); 498 } 499 500 501 static final String NAME = "Lax"; 502 503 } 504 505 506 507 // ==================================================================================== 508 509 510 /** A lighting sensor for a node view. 511 */ 512 final class Sensor extends Sensor1 implements NodalSensor 513 { 514 515 private Sensor() 516 { 517 Stage.i().lightBank().addSensor( Sensor.this ); 518 box.spool().add( new Hold() 519 { 520 public void release() { Stage.i().lightBank().removeSensor( Sensor.this ); } 521 }); 522 } 523 524 525 private void clear() 526 { 527 if( pollName == null ) 528 { 529 assert personName == null; 530 return; // already clear 531 } 532 533 displayTitle = null; 534 personName = null; 535 pollName = null; 536 changed(); 537 } 538 539 540 private void set( final CountJS count, final CountNodeJS node ) 541 { 542 if( count == null || node == null ) 543 { 544 clear(); 545 return; 546 } 547 548 displayTitle = node.displayTitle(); 549 personName = node.name(); 550 pollName = count.pollName(); 551 changed(); 552 } 553 554 555 /** The node view associated with this lighting sensor. 556 */ 557 NodeV view() { return NodeV.this; } 558 559 560 // - N o d a l - S e n s o r ------------------------------------------------------ 561 562 563 public NodeV.Box box() { return box; } 564 565 566 public String displayTitle() { return displayTitle; } 567 568 569 private String displayTitle; 570 571 572 // - P o s i t i o n - S e n s o r ------------------------------------------------ 573 574 575 public String personName() { return personName; } 576 577 578 private String personName; 579 580 581 public String pollName() { return pollName; } 582 583 584 private String pollName; 585 586 } 587 588 589 590//// P r i v a t e /////////////////////////////////////////////////////////////////////// 591 592 593 private ArrowSegment arrowSegment() { return (ArrowSegment)getFirstChild(); } 594 595 596 597 private CycleSpot cycleSpot() { return (CycleSpot)getLastChild(); } 598 599 600 601 private final Highlighter highlighter; 602 603 604 605 private boolean isClickable; 606 607 608 609 /** Answers whether the node is a {@linkplain CountNode#isCycler cycler}, returning 610 * false if the node is null. 611 */ 612 private boolean isCycler() { return countNode == null? false: countNode.isCycler(); } 613 614 615 private void setCycler() { cycleSpot().getStyle().setDisplay( INLINE ); } 616 617 618 private void clearCycler() { cycleSpot().getStyle().clearDisplay(); } 619 // default to "none" as set in vote/track.css 620 621 622 623 private final Sensor sensor; 624 625 626 627 private final Tracker tracker; 628 629 630 631 // ==================================================================================== 632 633 634 private final class ArrowSegment extends OMSVGPathElement 635 { 636 637 ArrowSegment() 638 { 639 addClassNameBaseVal( "ArrowSegment" ); 640 final OMSVGPathSegList sL = getPathSegList(); 641 /*0*/sL.appendItem( createSVGPathSegMovetoAbs( 0,0 )); 642 /*1*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 )); 643 /*2*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 )); 644 /*3*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 )); 645 /*4*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 )); 646 /*5*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 )); 647 /*0*/sL.appendItem( createSVGPathSegClosePath() ); 648 } 649 650 651 void repaint( final float x, final float length, final float protrusion, final int y, 652 final float halfThickness ) 653 { 654 final OMSVGPathSegList sL = getPathSegList(); 655 656 // 0 1 657 // +--------------+ 658 // \ \ 659 // + 5 + 2 660 // / / 661 // +--------------+ 662 // 4 3 663 664 PathSeg.movetoAbs( sL, 0, x, y ); 665 PathSeg.lineToRel( sL, 1, length, 0 ); 666 PathSeg.lineToRel( sL, 2, protrusion, halfThickness ); 667 PathSeg.lineToRel( sL, 3, -protrusion, halfThickness ); 668 PathSeg.lineToRel( sL, 4, -length, 0 ); 669 PathSeg.lineToRel( sL, 5, protrusion, -halfThickness ); 670 } 671 672 } 673 674 675 676 // ==================================================================================== 677 678 679 private final class Clicker implements ClickHandler 680 { 681 682 Clicker() { addClickHandler( Clicker.this ); } // no need to unregister, registry does not outlive the handler 683 684 685 public void onClick( ClickEvent _e ) 686 { 687 if( !isClickable ) return; 688 689 String name = countNode.name(); 690 if( name.equals( Stage.i().getActorName() )) name = null; // toggle off 691 Stage.setActorName( name ); 692 } 693 694 } 695 696 697 698 // ==================================================================================== 699 700 701 private final class CycleSpot extends OMSVGCircleElement 702 { 703 704 CycleSpot() { addClassNameBaseVal( "CycleSpot" ); } 705 706 707 private static final float MAX_RADIUS = 2f; 708 709 710 void repaint( final float x, final float length, final float protrusion, final int y, 711 final float halfThickness ) 712 { 713 if( !isCycler() ) 714 { 715 assert !INLINE.getCssName().equals( getStyle().getDisplay() ); 716 return; // not displayed 717 } 718 719 final float spareWidth = length - MAX_RADIUS - STROKE_WIDTH; 720 final float r = spareWidth > 6? MAX_RADIUS: MAX_RADIUS/2; // less when very short 721 getR().getBaseVal().setValue( r ); 722 getCx().getBaseVal().setValue( x + length/2 + protrusion 723 - /*correction for perfect centering*/r ); 724 getCy().getBaseVal().setValue( y + halfThickness ); 725 } 726 727 } 728 729 730 731 // ==================================================================================== 732 733 734 private final class Highlighter implements PropertyChangeHandler 735 { 736 737 Highlighter() 738 { 739 box.spool().add( new Hold() 740 { 741 final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( 742 PropertyChange.TYPE, /*source*/NodeV.class, Highlighter.this ); 743 public void release() { hR.removeHandler(); } 744 }); 745 box.spool().add( new Hold() 746 { 747 final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( 748 PropertyChange.TYPE, /*source*/Stage.i(), Highlighter.this ); 749 public void release() { hR.removeHandler(); } 750 }); 751 // relight(); // init 752 /// no need to init, is called when node set 753 } 754 755 756 public final void onPropertyChange( final PropertyChange e ) 757 { 758 if( box.spool().isUnwinding() ) return; 759 760 final String name = e.propertyName(); 761 final Object source = e.getSource(); 762 if( source.equals( NodeV.class )) 763 { 764 if( name.equals( "deselectionGuard" )) relight(); 765 } 766 else 767 { 768 assert source.equals( Stage.i() ); 769 if( name.equals( "actorName" )) relight(); 770 } 771 } 772 773 774 final @Warning("init call") void relight() 775 { 776 isClickable = countNode != null; // generally 777 if( isClickable && countNode.name().equals( Stage.i().getActorName() )) 778 { 779 addClassNameBaseVal( "highlit" ); // if not already added 780 deselectionGuard.guard( NodeV.this ); 781 } 782 else removeClassNameBaseVal( "highlit" ); 783 if( isClickable ) addClassNameBaseVal( "clickable" ); 784 else removeClassNameBaseVal( "clickable" ); 785 } 786 787 } 788 789 790 791 // ==================================================================================== 792 793 794 private final class Mouser implements MouseOutHandler, MouseOverHandler 795 { 796 Mouser() 797 { 798 addMouseOutHandler( Mouser.this ); // no need to unregister, registry does not outlive the handler 799 addMouseOverHandler( Mouser.this ); // " 800 // Might instead add one handler to the 'svg' ancestor and catch the events 801 // as they bubble. That requires svg's pointer-events='none' and this 802 // element's pointer-events='visible'. The NodeV can then be resolved 803 // via event.getNativeEvent().getEventTarget()._get("__wrapper"). But then 804 // both event types (out and over) sporadically fail on Chrome (18). 805 } 806 807 808 public void onMouseOut( MouseOutEvent _e ) { sensor.out(); } 809 810 811 public void onMouseOver( MouseOverEvent _e ) { sensor.over(); } 812 813 } 814 815 816 817 // ==================================================================================== 818 819 820 private final class Tracker implements ChangeHandler 821 { 822 823 Tracker( final VoteTrack track ) 824 { 825 box.spool().add( new Hold() 826 { 827 final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( Change.TYPE, 828 /*source*/track, Tracker.this ); 829 public void release() { hR.removeHandler(); } 830 }); 831 // run( track ); // init 832 /// no need to init, is called when node set 833 } 834 835 836 private void clearAnchor() 837 { 838 if( !isAnchor ) return; 839 840 removeClassNameBaseVal( "anchor" ); 841 isAnchor = false; 842 } 843 844 845 private boolean isAnchor; 846 847 848 private void setAnchor() 849 { 850 if( isAnchor ) return; 851 852 addClassNameBaseVal( "anchor" ); 853 isAnchor = true; 854 } 855 856 857 private void clearTightCycler() 858 { 859 if( !isTightCycler ) return; 860 861 removeClassNameBaseVal( "y" ); 862 isTightCycler = false; 863 } 864 865 866 private void setTightCycler() 867 { 868 if( isTightCycler ) return; 869 870 addClassNameBaseVal( "y" ); 871 isTightCycler = true; 872 } 873 874 875 public void onChange( final Change e ) { run( (VoteTrack)e.getSource() ); } 876 877 878 final @Warning("init call") void run( final VoteTrack track ) 879 { 880 final CountNodeJS anchor = track.anchor(); 881 if( anchor == null ) 882 { 883 clearAnchor(); 884 clearTightCycler(); 885 return; 886 } 887 888 final XCastRelation place = box.place(); 889 if( place == CO_VOTER ) 890 { 891 if( countNode != null && countNode.dartSector() == anchor.dartSector() ) setAnchor(); 892 else clearAnchor(); 893 clearTightCycler(); 894 } 895 else 896 { 897 clearAnchor(); 898 if( isCycler() ) 899 { 900 if( place == VOTER && countNode.name().equals(anchor.candidateName()) 901 || place == CANDIDATE && anchor.name().equals(countNode.candidateName()) ) 902 { 903 assert anchor.isVoter(); 904 assert countNode.isVoter(); /* both must cast for tight cycle, but 905 this follows from isCycler and nominal checks above */ 906 setTightCycler(); 907 return; 908 } 909 } 910 911 clearTightCycler(); 912 } 913 } 914 915 } 916 917 918}