package 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. import com.google.gwt.core.client.*; import com.google.gwt.dom.client.*; import com.google.gwt.uibinder.client.*; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.*; import com.google.web.bindery.event.shared.HandlerRegistration; import java.util.*; import org.vectomatic.dom.svg.*; import org.vectomatic.dom.svg.utils.OMSVGParser; import votorola.a.count.gwt.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.web.gwt.*; import votorola.g.web.gwt.event.*; import votorola.s.gwt.stage.*; import static votorola.a.count.XCastRelation.VOTER; import static votorola.a.count.XCastRelation.CO_VOTER; /** A view of a {@linkplain VoteTrack vote track}. It shows the anchor node's {@linkplain * VoteTrack#voters() voters} left, {@linkplain VoteTrack#peers() peers} center, and * candidate right. The anchor node itself may have a {@linkplain BoardV pin indicator}. * Vote flow is depicted left to right with the volume indicated by numbers.
  *
  *         votersV                    peersV            candidateV
  *             \                        |                  /
  *    >>>>>>>>>>>>>>>>>>>>> --- >>>>>>>>>>>>>>>>>>>> --- >>>>>
  *                           |              /\        |
  *                      flow volume         /    flow volume
  *                                        pin
* *

When the track is invisible, e.g. because no poll is staged, then a {@linkplain * Podium podium} may become visible to serve as a staging control for the actor.

                                            ( )
  *                                              \
  *                                             podium
* *

Acknowledgement: The segmented arrow motif is borrowed from Christian * Weilbach's design of a progress track for quantitative summation accounts. See his * mock-ups 2b and 2g.

* * @see Guiding mock-ups * @see VoteTrackV.ui.xml */ public final class VoteTrackV extends Composite { /** Constructs a VoteTrackV. * * @param isBottomFixed answers whether this VoteTrackV is to be the {@linkplain * #iBottomFixed bottom-fixed instance}. You may create at most one * bottom-fixed instance. */ public VoteTrackV( VoteTrack _track, final boolean isBottomFixed ) { track = _track; if( isBottomFixed ) { if( iBottomFixed != null ) throw new IllegalArgumentException( "duplicate instance" ); iBottomFixed = VoteTrackV.this; } final UiBinderI uiBinder = GWT.create( UiBinderI.class ); initWidget( uiBinder.createAndBindUi( VoteTrackV.this )); if( DifferenceLight.sceneName() != null ) addStyleName( "diffScene" ); if( DartLight.i() == null ) new DartLight(); // Main components. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - initB( votersBoardV = new BoardV( track.voters(), VoteTrackV.this, vBoardElement, VOTER )); initB( peersBoardV = new PeerBoardV( track.peers(), VoteTrackV.this, pBoardElement )); init( candidateV = new CandidateV( VoteTrackV.this, cElement )); init( new FlowVolumeV( track.anchorHolder(), VoteTrackV.this, vOutflowElement, VOTER )); init( new FlowVolumeV( track.candidateHolder(), VoteTrackV.this, pOutflowElement, CO_VOTER)); // Overlay. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final OMSVGSVGElement svg = OMSVGParser.createDocument().createSVGSVGElement(); overlayDiv.appendChild( svg.getElement() ); svg.appendChild( new Podium( peersBoardV )); } /** The {@linkplain StageV staged instance} of VoteTrackV, or null if none is staged. */ public static VoteTrackV i( final StageV stageV ) { final Iterator ww = stageV.iterator(); while( ww.hasNext() ) { final Widget w = ww.next(); if( w instanceof VoteTrackV ) return (VoteTrackV)w; } return null; } /** The instance of VoteTrackV that is fixed to the bottom of the viewport, or null if * there is none. */ static VoteTrackV iBottomFixed() { return iBottomFixed; } private static VoteTrackV iBottomFixed; private void init( final MajorV child ) { // A problem with UIBinder (2.4) is the reason for adding these child widgets by // their elements. UIBinder apparently cannot substitute widgets for table // elements in an HTMLPanel. Explicitly specifying (for example) in the // template instead of results in a TypeError from JavaScript: "[object // DOMWindow] has no method 'replaceChild'" (Chrome) or "this.replaceChild is not // a function" (FireFox). final HTMLPanel y = getWidget(); y.add( child, /*parent to add to*/child.getElement().getParentElement() ); // element already attached to that parent obviously, but there's no // addWidgetOnly method. addAndReplaceElement used to work till 2.5, now it // silently rejects the widget if its element == element to replace } private void initB( final BoardV child ) { child.init(); init( child ); } @Warning("non-API") interface UiBinderI extends UiBinder {} // ` e a r l y ```````````````````````````````````````````````````````````````````````` private final Spool spool = new Spool1(); // ------------------------------------------------------------------------------------ /** The view of the {@linkplain VoteTrack#candidate candidate}. */ public CandidateV candidateV() { return candidateV; } private final CandidateV candidateV; @UiField @Warning("non-API") TableCellElement cElement; /** Answers whether this view is physically moving in order to visualize a change of * anchor and a consequent logical shift in votespace. The return value is bound to * property name moving. Components themselves ought to withold showing a * change of anchor or related changes till immediately after the motion has stopped. * Each major component must also explicitly {@linkplain MajorV#setVisible(boolean) * reshow} itself after the changes are rendered, as this view may have hidden it at * some point as part of the motion. */ public boolean isMoving() { return animation != null; }; private Animation animation; private void setMoving( final Animation newAnimation ) { if( animation != null || newAnimation == null ) { assert false; return; } animation = newAnimation; fireEvent( new PropertyChange( "moving" )); } private void clearMoving() { if( animation == null ) { assert false; return; } animation = null; fireEvent( new PropertyChange( "moving" )); } /** The view of the {@linkplain VoteTrack#peers peers board}. */ public PeerBoardV peersBoardV() { return peersBoardV; } private final PeerBoardV peersBoardV; @UiField @Warning("non-API") TableCellElement pElement; @UiField @Warning("non-API") TableCellElement pBoardElement; @UiField @Warning("non-API") TableCellElement pOutflowElement; /** The track on which this view is modelled. */ public VoteTrack track() { return track; } private final VoteTrack track; /** The view of the {@linkplain VoteTrack#voters voters board}. */ public BoardV votersBoardV() { return votersBoardV; } private final BoardV votersBoardV; @UiField @Warning("non-API") TableCellElement vElement; @UiField @Warning("non-API") TableCellElement vBoardElement; @UiField @Warning("non-API") TableCellElement vOutflowElement; // - C o m p o s i t e ---------------------------------------------------------------- protected @Override HTMLPanel getWidget() { return (HTMLPanel)super.getWidget(); } // - W i d g e t ---------------------------------------------------------------------- protected @Override void onLoad() { if( !spool.isUnwinding() ) { Stage.i().addInitializer( new TheatreInitializerC() // auto-removed { public void initComplete( final Stage stage, boolean _rPending ) { if( spool.isUnwinding() ) return; // assume here (for sake of simplicity) that default will not change: if( !VoteTrack.toPin(stage) ) new Animator( stage ); } }); } super.onLoad(); } protected @Override void onUnload() { super.onUnload(); spool.unwind(); } public @Override void removeFromParent() { if( getParent() != null ) throw new UnsupportedOperationException(); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static DartLight dartLight; @UiFactory @Warning("non-API") HeadsUpDisplay newHeadsUpDisplay() { return new HeadsUpDisplay( VoteTrackV.this, spool ); } @UiField @Warning("non-API") DivElement overlayDiv; @UiField @Warning("non-API") TableElement tableElement; // outermost table // ==================================================================================== private final class Animation extends Timer // OPT instead use AnimationScheduler { Animation( int _direction ) { direction = _direction; setMoving( Animation.this ); msStart = System.currentTimeMillis(); rate_percentPerMS = /*td.voters width per vote/track.css*/47d / MS_DURATION * direction; schedule( MS_PERIOD / 2 ); /* for first frame, which has faster start time for sake of accuracy in face of granular timing */ } private final int direction; // of track motion, -1 or 1 private Runnable frame = new Runnable() { public void run() // first frame only { if( direction == -1 ) candidateV.setVisible( false ); // size and position inaccurate, eagerly hide on leftward slide frame = new Runnable() { public void run() // all subsequent frames, and also called into first frame { final int msLapse = (int)(System.currentTimeMillis() - msStart); if( msLapse >= MS_STOP ) { move( MS_DURATION ); /* do not stop part way even if msLapse is less because final frame may hold for longer than others, calling attention to its misplacement */ stop( /*isImmediate*/false ); } else move( msLapse ); } }; frame.run(); if( !isCancelled ) scheduleRepeating( MS_PERIOD ); // for subsequent frames } }; private boolean isCancelled; private boolean isStopped; private void move( final int msLapse ) { final double distance = msLapse * rate_percentPerMS; tableStyle.setLeft( distance, Style.Unit.PCT ); } private static final int MS_DURATION = 200; private static final int MS_PERIOD = 25; // needn't be especially smooth private static final int MS_STOP = MS_DURATION - MS_PERIOD / 2; // faster end time, more accurate, correcting for granularity private final long msStart; private final double rate_percentPerMS; // speed of motion void stop( final boolean isImmediate ) /* "stop" rather than "cancel" because cancel() is called by superclass on scheduling before the first tick */ { cancel(); isCancelled = true; if( isImmediate ) stop2(); else Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand() { public void execute() /* after browser renders the previous motion. This delay tends to fail in dev mode, making the end of the animation jumpy; but it works in production mode */ { // Hide components till they re-render correctly. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for( final Widget component: getWidget() ) { // before clearing motion and restoring original left position of // view, hide each component till it finishes re-rendering in // response to clearMoving. See isMoving if( component instanceof MajorV ) component.setVisible( false ); } // Fade in components that show new information. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final ArrayList fades = new ArrayList( /*initial capacity*/2 ); if( direction == 1 ) fades.add( vElement ); else { fades.add( pElement ); fades.add( cElement ); } for( Element e: fades ) e.addClassName( "fadedOut" ); // begin faded out Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand() { public void execute() // after browser renders fadedOut { for( Element e: fades ) e.removeClassName( "fadedOut" ); // fade in } }); // - - - stop2(); } }); } private void stop2() { isStopped = true; tableStyle.clearLeft(); clearMoving(); } private final Style tableStyle = tableElement.getStyle(); // - T i m e r -------------------------------------------------------------------- public void run() { if( isStopped || spool.isUnwinding() ) return; assert animation == Animation.this; frame.run(); } } // ==================================================================================== private final class Animator implements ChangeHandler, PropertyChangeHandler { Animator( final Stage stage ) { // assert !VoteTrack.toPin(stage): "animator not enabled for pinned track"; // // else "animate" style hides overflow, and BoardV.pinImg won't quite render /// no longer a problem with div.animationBed addStyleName( "animate" ); spool.add( new Hold() { final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( Change.TYPE, /*source*/track, Animator.this ); public void release() { hR.removeHandler(); } }); spool.add( new Hold() { final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/stage, Animator.this ); public void release() { hR.removeHandler(); } }); spool.add( new Hold() { public void release() { if( animation != null ) animation.cancel(); } }); anchorLast = track.anchor(); } private CountNodeJS anchorLast; public void onChange( Change _e ) { if( spool.isUnwinding() ) return; final CountNodeJS anchor = track.anchor(); anchorLast = anchor; } public void onPropertyChange( final PropertyChange e ) { // Motion is here triggered by change events that issue immediately from the // stage. Component views learn of these events somewhat later because of the // delayed intermediation of VoteTrack.gun. Motion will have commenced by // then and the component views will therefore postpone their remodelling till // after the motion stops. See isMoving(). if( !"actorName".equals(e.propertyName()) || spool.isUnwinding() ) return; if( animation != null ) { animation.stop( /*isImmediate*/true ); // actor changed in mid-motion, abort motion return; // do not try to re-animate, just do an immediate transition } if( anchorLast == null ) return; final String anchorName = VoteTrack.anchorName( Stage.i() ); if( anchorName == null ) return; if( anchorName.equals( anchorLast.candidateName() )) new Animation( /*stage left*/-1 ); else if( CountNodeJS.findNode(anchorName,anchorLast.voters()) != null ) { new Animation( /*stage right*/1 ); } } } }