001package votorola.s.gwt.stage.link; // Copyright 2011-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.dom.client.*;
005import com.google.gwt.http.client.URL;
006import com.google.gwt.uibinder.client.*;
007import com.google.gwt.user.client.ui.*;
008import com.google.web.bindery.event.shared.HandlerRegistration;
009import java.util.Iterator;
010import java.util.regex.*;
011import votorola.a.diff.*;
012import votorola.a.position.*;
013import votorola.a.voter.*;
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.s.gwt.stage.*;
020
021
022/** A view of a link track.  The view is divided horizontally into two sides and a middle.
023  * The two sides are occupied by links and other tools, while the middle is occupied by
024  * two captions for the display of text.  The two captions may overlap, the left being
025  * partly eclipsed by the right whenever the combined content of both is too wide.<pre
026  *
027 *>    +-----------------------------------------------------------+
028  *    |    side    | caption                 caption |    side    |
029  *    +-----------------------------------------------------------+</pre>
030  *
031  *     @see <a href='../../../../../../../s/gwt/stage/link/LinkTrackV.ui.xml'
032  *                                                        >LinkTrackV.ui.xml</a>
033  */
034public final class LinkTrackV extends Composite
035{
036
037
038    /** Constructs a LinkTrackV.
039      */
040    LinkTrackV()
041    {
042        DraftPageType type = DraftPageType.nonDraft; // till proven otherwise
043        final JsArrayString categories = WindowX.js()._get( "wgCategories" );
044        if( categories != null ) for( int c = categories.length() - 1; c >= 0; --c )
045        {
046            final String cat = categories.get( c );
047            if( "Remote draft".equals( cat ))
048            {
049                type = DraftPageType.remoteDraft;
050                break;
051            }
052            else if( "Draft".equals( cat ))
053            {
054                type = DraftPageType.localDraft; // assuming this is pollwiki, per s/gwt/pollwiki
055                break;
056            }
057        }
058        // else not a MediaWiki page
059        draftPageType = type;
060    }
061
062
063
064    /** Returns the {@linkplain StageV staged instance} of LinkTrackV, or null if none is
065      * staged.
066      */
067    public static LinkTrackV i( final StageV stageV )
068    {
069        final Iterator<Widget> ww = stageV.iterator();
070        while( ww.hasNext() )
071        {
072            final Widget w = ww.next();
073            if( w instanceof LinkTrackV ) return (LinkTrackV)w;
074        }
075
076        return null;
077    }
078
079
080
081   // ` e a r l y ````````````````````````````````````````````````````````````````````````
082
083
084    private final Spool spool = new Spool1();
085
086
087
088    @Warning("non-API") interface UiBinderI extends UiBinder<Widget,LinkTrackV> {}
089
090        {
091            final UiBinderI uiBinder = GWT.create( UiBinderI.class );
092            initWidget( uiBinder.createAndBindUi( LinkTrackV.this ));
093        }
094
095
096
097   // ------------------------------------------------------------------------------------
098
099
100    /** The link to the {@linkplain Stage#getActorName actor}'s user page in the pollwiki.
101      */
102    public ActorLink actorLink() { return actorLink; }
103
104
105        private final ActorLink actorLink;
106
107
108        @UiField @Warning("non-API") AnchorElement actorA;
109
110        {
111            final String imageName = "actor";
112            init( actorA, imageName, App.i().mesS().gwt_stage_LinkTrack_actorTitle() );
113            actorLink = new ActorLink( actorA, imageName );
114            new Targeter1( actorA, "actorName" )
115            {
116                @Override void retarget()
117                {
118                    final String p = Stage.i().getActorName();
119                    setTarget( p == null? null:
120                      adjustedTarget(App.i().pollwiki().encodePageSpecifier("User:"+p).toString()) );
121                }
122            };
123        }
124
125
126
127    /** Adds a tool to the left side of this view.  This is only for temporary support of
128      * configuration item {@linkplain votorola.s.gwt.scene.Scenes#toUseRAC() toUseRAC}.
129      */
130    public @Warning("non-API") void addLeftTool( final Widget tool )
131    {
132        ((HTMLPanel)getWidget()).add( tool, /*containing element ID*/"LinkTrack-left" );
133    }
134
135
136
137    /** The link to the {@linkplain votorola.s.wic.diff.WP_D bridge scene} for the staged
138      * {@linkplain Stage#getActorName difference}.
139      */
140    public ActorLink diffLink() { return diffLink; }
141
142
143        private final ActorLink diffLink;
144
145
146        @UiField @Warning("non-API") AnchorElement diffA;
147
148        {
149            final String imageName = "diff";
150            init( diffA, imageName, App.i().mesS().gwt_stage_LinkTrack_diffTitle() );
151            diffLink = new ActorLink( diffA, imageName );
152            if( NominalDifferenceTargeter.isEnabled() )
153            {
154                new NominalDifferenceTargeter( LinkTrackV.this, diffA );
155            }
156            else new DifferenceTargeter( LinkTrackV.this, diffA );
157        }
158
159
160
161    /** The link to the {@linkplain Stage#getActorName actor}'s position draft.
162      */
163    public ActorLink draftLink() { return draftLink; }
164
165
166        private final ActorLink draftLink;
167
168
169        @UiField @Warning("non-API") AnchorElement draftA;
170
171        {
172            final String imageName = "draft";
173            init( draftA, imageName, App.i().mesS().gwt_stage_LinkTrack_draftTitle() );
174            draftLink = new ActorLink( draftA, imageName );
175            new TargeterP( draftA )
176            {
177                @Override void retarget()
178                {
179                    final Stage stage = Stage.i();
180                    final String actorName = stage.getActorName();
181                    final String pollName = stage.getPollName();
182                    if( pollName == null || actorName == null ) setTarget( null );
183                    else
184                    {
185                        final String contextPersonName;
186                        if( draftPageType == DraftPageType.localDraft )
187                        {
188                            final PositionID position = App.i().pollwiki().identifyAsPosition(
189                              Document.get() );
190                            contextPersonName = position.personName();
191                            if( position != null && actorName.equals(contextPersonName)
192                              && pollName.equals(position.pollName()) )
193                            {
194                                setTarget( null ); // already on actor's local draft page
195                                return;
196                            }
197                        }
198                        else if( draftPageType == DraftPageType.remoteDraft )
199                        {
200                            final Document doc = Document.get();
201                            Element marker;
202                            marker = doc.getElementById( "voDraft-author" );
203                            if( marker != null )
204                            {
205                                contextPersonName = IDPair.normalUsername( marker.getTitle() );
206                                if( actorName.equals( contextPersonName ))
207                                {
208                                    marker = doc.getElementById( "voDraft-poll" );
209                                    if( marker != null && pollName.equals( marker.getTitle() ))
210                                    {
211                                        setTarget( null ); // already on actor's remote draft page
212                                        return;
213                                    }
214                                }
215                            }
216                            else contextPersonName = stage.getDefaultActorName();
217                        }
218                        else contextPersonName = stage.getDefaultActorName();
219                        final StringBuilder b = GWTX.stringBuilderClear();
220                        b.append( App.getServletContextLocation() );
221                        b.append( "/w/Draft?p=" );
222                        b.append( pollName.replace( '/', '!' ));
223                        b.append( "&u=" );
224                        b.append( URL.encodeQueryString( actorName ));
225                        if( !( contextPersonName == null || contextPersonName.equals(actorName) ))
226                        {
227                            b.append( "&cP=" );
228                            b.append( URL.encodeQueryString( contextPersonName ));
229                        }
230                        setTarget( adjustedTarget( b.toString() ));
231                    }
232                }
233            };
234        }
235
236
237
238    /** The style variant for actor dependent links, or null if the ordinary style is
239      * used.
240      *
241      *     @see #clearActorLinkVariant(String)
242      *     @see #setActorLinkVariant(String)
243      */
244    public String getActorLinkVariant() { return actorLinkVariant; }
245
246
247        private String actorLinkVariant;
248
249
250        /** Clears the current style variant if it equals the one specified.
251          *
252          *     @see #getActorLinkVariant()
253          */
254        public void clearActorLinkVariant( final String variant )
255        {
256            if( actorLinkVariant == null || !actorLinkVariant.equals( variant )) return;
257              // clobber guard
258
259            ElementX.replaceNullClassName( actorLinkVariant, /*new*/null, getWidget().getElement() );
260            actorLinkVariant = null;
261            syncActorLinkVariant();
262        }
263
264
265        /** Sets the style variant for actor dependent links.  A null value forcefully
266          * clears the variant.  Consider using {@linkplain #clearActorLinkVariant(String)
267          * clearActorLinkVariant} instead to guard against prematurely clearing what was
268          * set by another caller.
269          *
270          *     @see #getActorLinkVariant()
271          *     @see ActorLink#resetVariant()
272          */
273        public void setActorLinkVariant( final String variant )
274        {
275            ElementX.replaceNullClassName( actorLinkVariant, /*new*/variant,
276              getWidget().getElement() ); /* could OPT by returning if not actually
277              replaced and we knew no resetVariant had been called */
278
279            actorLinkVariant = variant;
280            syncActorLinkVariant();
281        }
282
283
284        private void syncActorLinkVariant()
285        {
286            src( actorA, "actor", actorLinkVariant );
287            src( diffA, "diff", actorLinkVariant );
288            src( draftA, "draft", actorLinkVariant );
289        }
290
291
292
293   // - W i d g e t ----------------------------------------------------------------------
294
295
296    protected @Override void onUnload()
297    {
298        super.onUnload();
299        spool.unwind();
300    }
301
302
303
304   // ====================================================================================
305
306
307    /** An actor dependent link, the target of which typically depends on the staged
308      * actor.
309      */
310    public final class ActorLink
311    {
312
313        private ActorLink( AnchorElement _a, String _imageName )
314        {
315            a = _a;
316            imageName = _imageName;
317        }
318
319
320        private final AnchorElement a;
321
322
323        private final String imageName;
324
325
326       // --------------------------------------------------------------------------------
327
328
329        /** Forcefully clears the style variant for this actor link.
330          *
331          *     @see #getActorLinkVariant()
332          */
333        public void resetVariant() { src( a, imageName, /*variant*/null ); }
334
335    }
336
337
338
339   // ====================================================================================
340
341
342    abstract class StageListener extends TheatreInitializer0 implements PropertyChangeHandler
343    {
344
345        StageListener()
346        {
347            Scheduler.get().scheduleFinally( new Scheduler.ScheduledCommand()
348            {
349                public void execute()
350                {
351                    // delayed else init may be called before subclasses constructed
352                    Stage.i().addInitializer( StageListener.this ); // auto-removed
353                }
354            });
355        }
356
357
358        /** Called when the stage is completely initialized, adds listeners and
359          * initializes state.
360          *
361          *     returns false if init is aborted because the spool is unwinding, true
362          *       otherwise.
363          */
364        boolean init()
365        {
366            if( spool.isUnwinding() ) return false;
367
368            spool.add( new Hold()
369            {
370                final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
371                  PropertyChange.TYPE, /*source*/Stage.i(), StageListener.this );
372                public void release() { hR.removeHandler(); }
373            });
374            return true;
375        }
376
377
378       // - P r o p e r t y - C h a n g e - H a n d l e r --------------------------------
379
380
381        public void onPropertyChange( PropertyChange _e )
382        {
383            // GWT BUG (2.4).  Method must be defined here in base class, else
384            // NoSuchMethodError may occur on page load (at least in devmode), though that
385            // should be impossible.
386            throw new IllegalStateException(); // always overridden by subclass
387        }
388
389
390       // - T h e a t r e - I n i t i a l i z e r ----------------------------------------
391
392
393        public @Override final void initFromComplete( Stage _s, boolean _rPending ) { init(); }
394
395
396        public @Override final void initToComplete( Stage _s, boolean _rPending ) { init(); }
397
398    }
399
400
401
402   // ====================================================================================
403
404
405    /** An agent to adjust the target and enabled state of a link based on one or more
406      * stage properties.
407      */
408    abstract class Targeter extends StageListener
409    {
410
411        Targeter( AnchorElement _link ) { link = _link; }
412
413
414        @Override boolean init()
415        {
416            if( !super.init() ) return false;
417
418            retarget(); // init state
419            return true;
420        }
421
422
423       // --------------------------------------------------------------------------------
424
425
426        /** Returns the href adjusted by (a) nulling if it matches the current document
427          * location; or (b) insertion of dev mode parameter if dev mode is in effect.
428          */
429        final String adjustedTarget( final String href )
430        {
431            if( href == null ) return null;
432
433            String loc = Document.get().getURL();
434            final com.google.gwt.regexp.shared.MatchResult devModeLoc;
435            if( GWT.isProdMode() ) devModeLoc = null;
436            else // probably dev mode
437            {
438                devModeLoc = GWTX.DEV_MODE_LOCATION_PATTERN.exec( loc );
439                if( devModeLoc != null ) // then remove the devmode parameter
440                {
441                    final String trailer = devModeLoc.getGroup( 3 );
442                    if( trailer == null || trailer.length() == 0 )
443                    {
444                        final String leader = devModeLoc.getGroup( 1 );
445                        loc = leader.substring( 0, leader.length() - 1 ); // chop trailing ?|&
446                    }
447                    else loc = devModeLoc.getGroup(1) + trailer;
448                }
449            }
450            final String hrefA; // adjusted
451            if( href.equals( loc )) hrefA = null;
452            else
453            {
454                if( devModeLoc == null ) hrefA = href;
455                else if( href.lastIndexOf('#') > 0 ) hrefA = href; // cannot yet handle fragment
456                else
457                {
458                    final char delimeter = href.lastIndexOf('?') > 0? '&': '?';
459                    hrefA = href + delimeter + "gwt.codesvr=" + devModeLoc.getGroup( 2 );
460                }
461            }
462            return hrefA;
463        }
464
465
466        private final AnchorElement link;
467
468
469        abstract void retarget();
470
471
472        final void setTarget( final String href )
473        {
474            if( href == null )
475            {
476                link.addClassName( "disabled" );
477                link.removeClassName( "enabled" );
478                link.removeAttribute( "href" ); // actually disable it
479            }
480            else
481            {
482                link.addClassName( "enabled" );
483                link.removeClassName( "disabled" );
484                link.setAttribute( "href", href );
485            }
486        }
487
488    }
489
490
491
492   // ====================================================================================
493
494
495    /** A targeter that depends on a single stage property.
496      */
497    abstract class Targeter1 extends Targeter
498    {
499
500        Targeter1( AnchorElement _link, String _propertyName )
501        {
502            super( _link );
503            propertyName = _propertyName;
504        }
505
506
507        private final String propertyName;
508
509
510       // - P r o p e r t y - C h a n g e - H a n d l e r --------------------------------
511
512
513        public final void onPropertyChange( final PropertyChange e )
514        {
515            if( e.propertyName().equals( propertyName )) { retarget(); }
516        }
517
518    }
519
520
521
522   // ====================================================================================
523
524
525    /** A targeter that depends on multiple stage properties.
526      */
527    abstract class TargeterN extends Targeter implements Scheduler.ScheduledCommand
528    {
529
530        TargeterN( AnchorElement _link ) { super( _link ); }
531
532
533        final CoalescingSchedulerS coalescer = new CoalescingSchedulerS(
534          CoalescingSchedulerS.FINALLY, TargeterN.this );
535
536
537       // - 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 ------------------------
538
539
540        public final void execute() { retarget(); }
541
542    }
543
544
545
546//// P r i v a t e ///////////////////////////////////////////////////////////////////////
547
548
549    @UiField @Warning("non-API") AnchorElement diffMeA;
550
551        {
552            init( diffMeA, "diff", App.i().mesS().gwt_stage_LinkTrack_diffMeTitle() );
553            new TargeterP( diffMeA )
554            {
555                @Override boolean init()
556                {
557                    if( !super.init() ) return false;
558
559                    final TargeterP handler = this;
560                    spool.add( new Hold()
561                    {
562                        final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
563                          PropertyChange.TYPE, /*source*/App.i(), handler );
564                        public void release() { hR.removeHandler(); }
565                    });
566                    return true;
567                }
568                public @Override void onPropertyChange( final PropertyChange e )
569                {
570                    if( e.getSource() == App.i() )
571                    {
572                        if( e.propertyName().equals( "username" )) coalescer.schedule();
573                          // which then calls retarget
574                    }
575                    else super.onPropertyChange( e );
576                }
577                @Override void retarget()
578                {
579                    final String pollName = Stage.i().getPollName();
580                    final String actorName = Stage.i().getActorName();
581                    if( pollName == null || actorName == null
582                     || actorName.equals( App.getUsername() )) // avoid self diff
583                    {
584                        setTarget( null );
585                    }
586                    else
587                    {
588                        setTarget( adjustedTarget( App.getServletContextLocation()
589                          + "/w/D?alterAuthor=" + URL.encodeQueryString(actorName)
590                          + "&poll=" + URL.encodeQueryString(pollName) ));
591                    }
592                }
593            };
594        }
595
596
597
598    @UiField @Warning("non-API") AnchorElement draftMeA;
599
600        {
601            init( draftMeA, "draft", App.i().mesS().gwt_stage_LinkTrack_draftMeTitle() );
602            new TargeterN( draftMeA )
603            {
604                @Override boolean init()
605                {
606                    if( !super.init() ) return false;
607
608                    final TargeterN handler = this;
609                    spool.add( new Hold()
610                    {
611                        final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
612                          PropertyChange.TYPE, /*source*/App.i(), handler );
613                        public void release() { hR.removeHandler(); }
614                    });
615                    return true;
616                }
617                public @Override void onPropertyChange( final PropertyChange e )
618                {
619                    final String name = e.propertyName();
620                    if( e.getSource() == App.i() )
621                    {
622                        if( name.equals( "username" )) coalescer.schedule(); // calls retarget
623                    }
624                    else if( name.equals( "pollName" )) coalescer.schedule(); // calls retarget
625                }
626                @Override void retarget()
627                {
628                    final String pollName = Stage.i().getPollName();
629                    if( pollName == null ) setTarget( null );
630                    else
631                    {
632                        if( draftPageType == DraftPageType.localDraft )
633                        {
634                            final String username = App.getUsername();
635                            if( username != null )
636                            {
637                                final PositionID position = App.i().pollwiki().identifyAsPosition(
638                                  Document.get() );
639                                if( position != null && username.equals(position.personName())
640                                  && pollName.equals(position.pollName()) )
641                                {
642                                    setTarget( null ); // already on user's local draft page
643                                    return;
644                                }
645                            }
646                        }
647                        // Else maybe remoteDraft.  Cannot currently ID user there.  Might
648                        // allow username in user CSS along with customization of stage
649                        // layout (per s.gwt.mediawiki.MediaWikiIn); but single sign on is
650                        // planned, and it's maybe better to wait and see the shape of it.
651                        setTarget( adjustedTarget( App.getServletContextLocation() + "/w/MyDraft?p="
652                          + pollName.replace('/','!') ));
653                    }
654                }
655            };
656        }
657
658
659
660    private final DraftPageType draftPageType;
661
662
663
664    private static void init( final AnchorElement a, final String imageName, final String title )
665    {
666        src( a, imageName, /*variant*/null );
667        a.setTitle( title );
668    }
669
670
671
672    @UiField @Warning("non-API") SpanElement leftCaption;
673
674
675
676    private final Text leftText = Document.get().createTextNode( "" );
677
678        {
679            leftCaption.appendChild( leftText );
680            new LeftCaptioner();
681        }
682
683
684
685    @UiField @Warning("non-API") AnchorElement messageA;
686
687        {
688            init( messageA, "message", App.i().mesS().gwt_stage_LinkTrack_messageTitle() );
689            new Targeter1( messageA, "message" )
690            {
691                @Override void retarget()
692                {
693                    final Message m = Stage.i().getMessage();
694                    setTarget( m == null? null: m.location() );
695                }
696            };
697        }
698
699
700
701    @UiField @Warning("non-API") AnchorElement pollA;
702
703        {
704            init( pollA, "poll", App.i().mesS().gwt_stage_LinkTrack_pollTitle() );
705            new Targeter1( pollA, "pollName" )
706            {
707                @Override void retarget()
708                {
709                    final String p = Stage.i().getPollName();
710                    setTarget( p == null? null: adjustedTarget(
711                      App.i().pollwiki().encodePageSpecifier(p).toString()) );
712                }
713            };
714        }
715
716
717
718    @UiField @Warning("non-API") SpanElement rightCaption;
719
720
721
722    private final Text rightText = Document.get().createTextNode( "" );
723
724        {
725            rightCaption.appendChild( rightText );
726            new RightCaptioner();
727        }
728
729
730
731    private static void src( final AnchorElement a, final String imageName, final String variant )
732    {
733        final ImageElement img = a.getFirstChildElement().cast();
734        final StringBuilder b = GWTX.stringBuilderClear();
735        b.append( App.i().staticContextLocation() );
736        b.append( "/stage/link/" );
737        b.append( imageName );
738        if( variant != null )
739        {
740            assert variant.length() > 0;
741            b.append( '-' );
742            b.append( variant );
743        }
744        b.append( ".png" );
745        img.setAttribute( "src", b.toString() );
746    }
747
748
749
750    @UiField @Warning("non-API") AnchorElement voteA;
751
752        {
753            init( voteA, "vote", App.i().mesS().gwt_stage_LinkTrack_voteTitle() );
754            new TargeterP( voteA )
755            {
756                @Override void retarget()
757                {
758                    final String pollName = Stage.i().getPollName();
759                    if( pollName == null ) setTarget( null );
760                    else
761                    {
762                        String href = App.getServletContextLocation() + "/w/Votespace?p="
763                          + pollName.replace( '/', '!' );
764                        final String actorName = Stage.i().getActorName();
765                        if( actorName != null ) href += "&u=" + URL.encodeQueryString( actorName );
766                        setTarget( adjustedTarget( href ));
767                    }
768                }
769            };
770        }
771
772
773
774   // ====================================================================================
775
776
777    private static enum DraftPageType { localDraft, nonDraft, remoteDraft }
778      // http://reluk.ca/w/Category:Draft
779
780
781
782   // ====================================================================================
783
784
785    private final class LeftCaptioner extends StageListener
786    {
787
788        @Override boolean init()
789        {
790            if( !super.init() ) return false;
791
792            recaption(); // init state
793            return true;
794        }
795
796
797        private final @Warning("init call") void recaption()
798        {
799            final Stage s = Stage.i();
800            final Message m = s.getMessage(); // message takes priority
801            final String c = m == null? s.getPollName(): m.content(); // then poll
802            if( c == null ) leftText.deleteData( 0, leftText.getLength() );
803            else leftText.setData( c );
804        }
805
806
807       // - P r o p e r t y - C h a n g e - H a n d l e r --------------------------------
808
809
810        public final void onPropertyChange( final PropertyChange e )
811        {
812            final String pName = e.propertyName();
813            if( pName.equals("message") || pName.equals("pollName") ) { recaption(); }
814        }
815
816    }
817
818
819
820   // ====================================================================================
821
822
823    private final class RightCaptioner extends StageListener
824    {
825
826        @Override boolean init()
827        {
828            if( !super.init() ) return false;
829
830            recaption(); // init state
831            return true;
832        }
833
834
835        private final @Warning("init call") void recaption()
836        {
837            final String actorName = Stage.i().getActorName();
838            if( actorName == null ) rightText.deleteData( 0, rightText.getLength() );
839            else rightText.setData( actorName );
840        }
841
842
843       // - P r o p e r t y - C h a n g e - H a n d l e r --------------------------------
844
845
846        public final void onPropertyChange( final PropertyChange e )
847        {
848            if( e.propertyName().equals( "actorName" )) { recaption(); }
849        }
850
851    }
852
853
854
855   // ====================================================================================
856
857
858    /** A targeter that depends on what position is shown on stage.
859      */
860    private abstract class TargeterP extends TargeterN
861    {
862
863        TargeterP( AnchorElement _link ) { super( _link ); }
864
865
866       // - P r o p e r t y - C h a n g e - H a n d l e r --------------------------------
867
868
869        public void onPropertyChange( final PropertyChange e )
870        {
871            final String pName = e.propertyName();
872            if( pName.equals("pollName") || pName.equals("actorName") ) coalescer.schedule();
873              // which then calls retarget
874        }
875
876    }
877
878
879}