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.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.MouseOutEvent; import com.google.gwt.event.dom.client.MouseOutHandler; import com.google.gwt.event.dom.client.MouseOverEvent; import com.google.gwt.event.dom.client.MouseOverHandler; import com.google.gwt.event.shared.GwtEvent; import com.google.web.bindery.event.shared.HandlerRegistration; import org.vectomatic.dom.svg.*; import votorola.a.count.*; import votorola.a.count.gwt.*; import votorola.a.web.gwt.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.web.gwt.*; import votorola.g.web.gwt.event.*; import votorola.g.web.gwt.svg.*; import votorola.s.gwt.stage.*; import votorola.s.gwt.stage.light.*; import static com.google.gwt.dom.client.Style.Display.INLINE; import static votorola.a.count.XCastRelation.VOTER; import static votorola.a.count.XCastRelation.CO_VOTER; import static votorola.a.count.XCastRelation.CANDIDATE; /** A view of a count node in the shape of an arrow segment. Clicking on the view sets or * unsets the node as the {@linkplain Stage#getActorName() stage actor}.
* * protrusion * / * |---- length ---|--| * * p0 +---------------+ - * \ \ | half thickness * + * + - * / / * +---------------+* * If the node is a cycler, then a spot (*) is visible in the middle of the view. * * @see Acknowledgement to Christian Weilbach */ public class NodeV extends OMSVGGElement { /** Constructs a NodeV. * * @see #box() * @see #dartSector() */ NodeV( Box _box, int _dartSector, final VoteTrack track ) { // track needed only because box.trackV is null during init box = _box; dartSector = _dartSector; // View. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - addClassNameBaseVal( "CountNode" ); final XCastRelation place = box.place(); String s = Character.toString( place.symbol() ); if( place == XCastRelation.VOTER || place == XCastRelation.CO_VOTER ) s += dartSector; dartLightClassName = s; addClassNameBaseVal( dartLightClassName ); appendChild( new ArrowSegment() ); appendChild( new CycleSpot() ); // Controllers. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new Clicker(); highlighter = new Highlighter(); new Mouser(); sensor = new Sensor(); tracker = new Tracker( track ); } /** Forces static initialization of this class and appends a post-configuration * initializer to the specified spool. * * @param configurationSpool a spool that will unwind immediately after * {@linkplain StageMod module configuration} is complete. */ public static void forceInitClass( final Spool configurationSpool ) { configurationSpool.add( new Hold() { public void release() { if( deselectionGuard == null ) deselectionGuard = new DefaultDeselect(); } }); } // ------------------------------------------------------------------------------------ /** The ancestral container of this view, the {@linkplain MajorV#spool() spool} of * which controls its life cycle. */ public final Box box() { return box; } private final Box box; /** The CSS class name for the {@linkplain DartLight dart light}, such as "v17". */ final String dartLightClassName() { return dartLightClassName; } private final String dartLightClassName; /** The dart sector to which this view is restricted, or zero if it is not restricted * to any particular sector. * * @see votorola.a.count.CountNode#dartSector() */ final int dartSector() { return dartSector; } private final int dartSector; /** The count node on which this view is modelled, or null if none is modelled. * * @see #setCountNode(CountNodeJS) */ public final CountNodeJS getCountNode() { return countNode; } // bearing in mind this is a subclass of OMNode private CountNodeJS countNode; /** Sets the count node on which this view is modelled. * * @return true if setting the node resulted in a change, false if that node * was already set. * @see #getCountNode() * @throws IllegalArgumentException if the node does not have the same dart * sector as this view. */ final boolean setCountNode( final CountNodeJS node ) { if( ObjectX.nullEquals( node, countNode )) return false; // Guard against pointless repaint, though it is not currently expensive. // This is valid only because the node is immutable except its 'voters' // array, which has no bearing on the view. final VoteTrack track = box.trackV().track(); if( node == null ) { if( countNode != null ) removeClassNameBaseVal( "occupied" ); // it changed clearCycler(); sensor.clear(); } else { if( dartSector != 0 && node.dartSector() != dartSector ) { throw new IllegalArgumentException( "dart sector mismatch" ); } if( countNode == null ) addClassNameBaseVal( "occupied" ); // it changed if( node.isCycler() ) setCycler(); else clearCycler(); sensor.set( track.count(), node ); } countNode = node; tracker.run( track ); highlighter.relight(); return true; } /** Answers whether the node is in a tight cycle with the anchor. * * @see tight cycle */ final boolean isTightCycler() { return isTightCycler; } private boolean isTightCycler; /** The horizontal length of the top line of the drawing as currently drawn in the SVG * viewport. */ public final float length() { final OMSVGPathSegLinetoRel seg = (OMSVGPathSegLinetoRel) arrowSegment().getPathSegList().getItem( 1 ); return seg.getX(); } /** The point at the top left of the drawing as currently drawn in the SVG viewport. */ public final OMSVGPathSegMovetoAbs p0() { return (OMSVGPathSegMovetoAbs)arrowSegment().getPathSegList().getItem( 0 ); } /** The horizontal extent of the arrowhead beyond the {@linkplain #length() length}, * as currently drawn in the SVG viewport. */ public final float protrusion() { final OMSVGPathSegLinetoRel seg = (OMSVGPathSegLinetoRel) arrowSegment().getPathSegList().getItem( 2 ); return seg.getX(); } /** Redraws this view according to the provided parameters. The view never * automatically initiates a repaint, but depends entirely on its MajorV ancestor for * this. */ void repaint( final float x, float length, final float protrusion, final int y, final float halfThickness ) { arrowSegment().repaint( x, length, protrusion, y, halfThickness ); cycleSpot().repaint( x, length, protrusion, y, halfThickness ); } /** Sets the type of {@linkplain DeselectionGuard deselection guard}. Call this * method from the global configuration function {@linkplain StageMod * voGWTConfig.s_gwt_stage} in this fashion:
* * s_gwt_stage_vote_CountNodeV_setDeselectionGuard( 'Blind' ); * // default is 'Default'* * Or call it at any time during normal operation. * * @throws IllegalArgumentException if the provided guard name is unrecognized. */ public static @GWTConfigCallback void setDeselectionGuard( final String guardName ) { final DeselectionGuard g; if( BlindDeselect.NAME.equals( guardName )) g = new BlindDeselect(); else if( DefaultDeselect.NAME.equals( guardName )) g = new DefaultDeselect(); else if( LaxDeselect.NAME.equals( guardName )) g = new LaxDeselect(); else throw new IllegalArgumentException( "no such guard: " + guardName ); deselectionGuard = g; GWTX.i().bus().fireEventFromSource( new PropertyChange("deselectionGuard"), NodeV.class ); } /** The value is bound via the {@linkplain GWTX#bus() event bus} to property name * deselectionGuard on source NodeV.class. */ private static DeselectionGuard deselectionGuard; private static native void exposeDeselectionGuard() /*-{ $wnd.s_gwt_stage_vote_CountNodeV_setDeselectionGuard = $entry( @votorola.s.gwt.stage.vote.NodeV::setDeselectionGuard(Ljava/lang/String;) ); }-*/; static { assert StageMod.isForcedInit(): "forced init " + NodeV.class.getName(); exposeDeselectionGuard(); } /** The stroke width for painting the main figure (arrow segment). */ public static final int STROKE_WIDTH = 2; // from stage/vote/track.css // - O M - N o d e ------------------------------------------------------------------- public @Override final void fireEvent( final GwtEvent> e ) { try{ super.fireEvent( e ); } catch( Exception x ) { GWTX.handleUncaughtException( x ); } // q.v. for reason } public @Override final NodeV getNextSibling() { return (NodeV)super.getNextSibling(); } // auto and explicit {@inheritDoc} fail, JDK 1.7 public @Override final NodeV getPreviousSibling() // ditto {@inheritDoc} failure { return (NodeV)super.getPreviousSibling(); } // ==================================================================================== // /** The {@value NAME} deselection guard. It allows for deselection of nodes that are // * base candidates or non-participants (both being tracked at the base), provided no // * {@linkplain Stage#getDefaultActorName() default actor} is set. This allows the // * track to fall cleanly into its default state where only the base peer board is // * displayed. This will not normally happen when a default actor is set, of course, // * and that is why deselection is disabled in that case. // */ // static final class BaseCandidateDeselect implements DeselectionGuard // { // // public void guard( final NodeV nodeV ) // { // final CountNodeJS node = nodeV.countNode; // nodeV.isClickable = Stage.i().getDefaultActorName() == null && ( node.isBaseCandidate() // ||/*non-participant*/ !node.isVoter() && !node.isCandidate() ); // } // // // static final String NAME = "BaseCandidate"; // // } ///// But the track is pinned when a default actor is set, so its unclear why deselection ///// should be forbidden in that case. Being pinned, the track won't snap back far. // ==================================================================================== // /** The {@value NAME} deselection guard. It is the same as {@linkplain // * BaseCandidateDeselect BaseCandidateDeselect} except it always allows for the // * deselection of mosquitos. This is useful because there is ordinarily no way to // * navigate away from a mosquito once it is selected except by deselecting it. // */ // static final class BaseMosquitoDeselect implements DeselectionGuard // { // // public void guard( final NodeV nodeV ) // { // final CountNodeJS node = nodeV.countNode; // nodeV.isClickable = node.dartSector() == 0 // || Stage.i().getDefaultActorName() == null && node.isBaseCandidate(); // } // // // static final String NAME = "BaseMosquito"; // // } ///// But a mosquito may be deselected by selecting another node, so the use case is ///// unclear. Maybe the intention was to deselect a non-participant node, because ///// otherwise it may get stuck. // ==================================================================================== /** The {@value NAME} deselection guard. It allows for deselection of any node. If a * {@linkplain Stage#getDefaultActorName() default actor} is set, then it cannot * actually be deselected. But this guard will not prevent the attempt. */ static final class BlindDeselect implements DeselectionGuard { public void guard( final NodeV nodeV ) { nodeV.isClickable = true; } static final String NAME = "Blind"; } // ==================================================================================== /** A container of node views. */ public static abstract class Box extends MajorV { /** Creates a new Box. * * @see MajorV#place() * @see MajorV#trackV() */ Box( XCastRelation _place, VoteTrackV _trackV ) { super( _place, _trackV ); } // ------------------------------------------------------------------------------------ /** The nodal components of this view. */ public abstract Iterable