001package votorola.s.gwt.stage.vote; // Copyright 2013, Michael Allan, Christian Weilbach.  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.GWT;
004import com.google.gwt.dom.client.*;
005import com.google.gwt.event.dom.client.*;
006import com.google.gwt.uibinder.client.*;
007import com.google.gwt.user.client.ui.*;
008import votorola.a.count.gwt.*;
009import votorola.g.hold.*;
010import votorola.g.lang.*;
011import votorola.s.gwt.stage.*;
012import votorola.s.gwt.stage.light.*;
013
014import static com.google.gwt.dom.client.Style.Unit.PX;
015
016/** A projection above or below the mouse cursor showing the display title or personal
017  * name of the cued node.
018  */
019final class HeadsUpDisplay extends Composite implements Actuator<NodalSensor>, Light<NodalSensor>
020{
021
022
023    /** Creates a HeadsUpDisplay.  Create at most one for the entire life of the trackV
024      * registry, as currently it does not unregister its listeners there.
025      *
026      *     @param spool the spool for the release of associated holds.  When unwound it
027      *       releases the holds of this display and thereby disables it.
028      */
029    HeadsUpDisplay( VoteTrackV _trackV, final Spool spool )
030    {
031        trackV = _trackV;
032        if( trackV == VoteTrackV.iBottomFixed() )
033        {
034            rail = topRail;
035            shuttle = topShuttle;
036            text = topText;
037        }
038        else
039        {
040            rail = bottomRail;
041            shuttle = bottomShuttle;
042            text = bottomText;
043        }
044        final Element div = rail.getParentElement(); // layout div.display.top or bottom
045        div.addClassName( "inside" );
046        div.getStyle().setDisplay( Style.Display.BLOCK );
047        Stage.i().lightBank().addLight( HeadsUpDisplay.this );
048        spool.add( new Hold()
049        {
050            public void release() { Stage.i().lightBank().removeLight( HeadsUpDisplay.this ); }
051        });
052        new Shuttler();
053        redisplay();
054    }
055
056
057
058   // ` e a r l y ````````````````````````````````````````````````````````````````````````
059
060
061    @Warning("non-API") interface UiBinderI extends UiBinder<HTMLPanel,HeadsUpDisplay> {}
062
063        {
064            final UiBinderI uiBinder = GWT.create( UiBinderI.class );
065            initWidget( uiBinder.createAndBindUi( HeadsUpDisplay.this ));
066        }
067
068
069
070   // - A c t u a t o r ------------------------------------------------------------------
071
072
073    public void changed( final NodalSensor sensor )
074    {
075        if( sensor == litSensor ) redisplay();
076    }
077
078
079
080    public Light<NodalSensor> light() { return HeadsUpDisplay.this; }
081
082
083
084    public void out( final NodalSensor sensor )
085    {
086        if( sensor != litSensor ) return; // already out
087
088        litSensor = null;
089        redisplay();
090    }
091
092
093
094    public void over( final NodalSensor sensor )
095    {
096        if( sensor == litSensor ) return; // already over
097
098        litSensor = sensor;
099        redisplay();
100    }
101
102
103
104   // - C o m p o s i t e ----------------------------------------------------------------
105
106
107    protected @Override HTMLPanel getWidget() { return (HTMLPanel)super.getWidget(); }
108
109
110
111   // - L i g h t ------------------------------------------------------------------------
112
113
114    public Actuator<NodalSensor> assignActuator( final NodalSensor sensor )
115    {
116        return sensor.box().trackV() == trackV? HeadsUpDisplay.this: null;
117          // actuate only for sensors in the same track view
118    }
119
120
121
122    public final NodalSensor tryCast( final Sensor sensor )
123    {
124        return sensor instanceof NodalSensor? (NodalSensor)sensor: null;
125    }
126
127
128
129//// P r i v a t e ///////////////////////////////////////////////////////////////////////
130
131
132    private static final String BLANK_TEXT = "\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0";
133      // non-breaking spaces
134
135
136
137    @UiField @Warning("non-API") SpanElement bottomField;
138
139
140
141    @UiField @Warning("non-API") DivElement bottomRail;
142
143
144
145    @UiField @Warning("non-API") SpanElement bottomShuttle;
146
147
148
149    private final Text bottomText = Document.get().createTextNode( "" );
150
151        { bottomField.appendChild( bottomText ); }
152
153
154
155    private NodalSensor litSensor;
156
157
158
159    private final DivElement rail; // top or bottom, whichever is displayed
160
161
162
163    private @Warning("init call") void redisplay()
164    {
165        final Style style = getStyleElement().getStyle();
166        if( litSensor == null ) style.setOpacity( 0 ); // hide the display
167        else
168        {
169            String s = litSensor.displayTitle();
170            if( s == null )
171            {
172                s = litSensor.personName();
173                if( s == null ) s = BLANK_TEXT;
174            }
175            text.setData( s );
176            style.clearOpacity(); // to default, opaque and thus visible
177        }
178    }
179
180
181
182    private final SpanElement shuttle; // top or bottom, whichever is displayed
183
184
185
186    private final Text text; // top or bottom, whichever is displayed
187
188
189
190    @UiField @Warning("non-API") SpanElement topField;
191
192
193
194    @UiField @Warning("non-API") DivElement topRail;
195
196
197
198    @UiField @Warning("non-API") SpanElement topShuttle;
199
200
201
202    private final Text topText = Document.get().createTextNode( "" );
203
204        { topField.appendChild( topText ); }
205
206
207
208    private final VoteTrackV trackV;
209
210
211
212   // ====================================================================================
213
214
215    /** A controller to position this display.
216      */
217    private final class Shuttler implements MouseMoveHandler, MouseOverHandler
218    {
219
220        Shuttler()
221        {
222            trackV.addDomHandler( Shuttler.this, MouseMoveEvent.getType() ); // no need to unregister, registry does not outlive the handler
223            trackV.addDomHandler( Shuttler.this, MouseOverEvent.getType() ); // "
224        }
225
226
227        private void reposition( final MouseEvent<?> e )
228        {
229            final int left = e.getX() - /*aesthetic adjust*/1;
230         // if( left < 0 ) left = 0; // keep left field visible
231            shuttle.getStyle().setLeft( left , PX );
232            final int y = e.getY();
233              // Adjusting the Y position too, so the fields behave together as a
234              // supplementary cursor.  This helps the user to guide the mouse, which is
235              // otherwise difficult because the cursor in the vote track (unlike poll and
236              // talk tracks) is relatively small compared to the large, moving display
237              // fields, like a dog running alongside a pair of cattle.  Here we position
238              // the top field to abut the visible top of the node view (fill) when the
239              // cursor is at the bottom limit of the view, and vice versa for the bottom
240              // field.  The user need only keep them from touching the edges of the view.
241        //  topRail.getStyle().setTop( y + NodeVPainter.MARGIN_TOP
242        //    + NodeV.STROKE_WIDTH, PX );
243        //    // not sure why stroke needn't be halved here (Firefox 16)
244        //  bottomRail.getStyle().setTop( y - NodeVPainter.MARGIN_BOTTOM
245        //    - NodeV.STROKE_WIDTH / 2, PX );
246        //    // this may be off by one pixel for very large font sizes (Firefox 16)
247        //// but when SVG not absolutely positioned, it happens to be much simpler:
248            rail.getStyle().setTop( y, PX );
249        }
250
251
252       // - M o u s e - M o v e - H a n d l e r ------------------------------------------
253
254
255        public void onMouseMove( final MouseMoveEvent e ) { reposition( e ); }
256
257
258       // - M o u s e - O v e r - H a n d l e r ------------------------------------------
259
260
261        public void onMouseOver( final MouseOverEvent e ) { reposition( e ); }
262
263    }
264
265
266}