package 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. import com.google.gwt.core.client.*; import com.google.gwt.dom.client.*; import com.google.gwt.http.client.URL; import com.google.gwt.uibinder.client.*; import com.google.gwt.user.client.ui.*; import com.google.web.bindery.event.shared.HandlerRegistration; import java.util.Iterator; import java.util.regex.*; import votorola.a.diff.*; import votorola.a.position.*; import votorola.a.voter.*; 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.s.gwt.stage.*; /** A view of a link track. The view is divided horizontally into two sides and a middle. * The two sides are occupied by links and other tools, while the middle is occupied by * two captions for the display of text. The two captions may overlap, the left being * partly eclipsed by the right whenever the combined content of both is too wide.
    +-----------------------------------------------------------+
  *    |    side    | caption                 caption |    side    |
  *    +-----------------------------------------------------------+
* * @see LinkTrackV.ui.xml */ public final class LinkTrackV extends Composite { /** Constructs a LinkTrackV. */ LinkTrackV() { DraftPageType type = DraftPageType.nonDraft; // till proven otherwise final JsArrayString categories = WindowX.js()._get( "wgCategories" ); if( categories != null ) for( int c = categories.length() - 1; c >= 0; --c ) { final String cat = categories.get( c ); if( "Remote draft".equals( cat )) { type = DraftPageType.remoteDraft; break; } else if( "Draft".equals( cat )) { type = DraftPageType.localDraft; // assuming this is pollwiki, per s/gwt/pollwiki break; } } // else not a MediaWiki page draftPageType = type; } /** Returns the {@linkplain StageV staged instance} of LinkTrackV, or null if none is * staged. */ public static LinkTrackV i( final StageV stageV ) { final Iterator ww = stageV.iterator(); while( ww.hasNext() ) { final Widget w = ww.next(); if( w instanceof LinkTrackV ) return (LinkTrackV)w; } return null; } // ` e a r l y ```````````````````````````````````````````````````````````````````````` private final Spool spool = new Spool1(); @Warning("non-API") interface UiBinderI extends UiBinder {} { final UiBinderI uiBinder = GWT.create( UiBinderI.class ); initWidget( uiBinder.createAndBindUi( LinkTrackV.this )); } // ------------------------------------------------------------------------------------ /** The link to the {@linkplain Stage#getActorName actor}'s user page in the pollwiki. */ public ActorLink actorLink() { return actorLink; } private final ActorLink actorLink; @UiField @Warning("non-API") AnchorElement actorA; { final String imageName = "actor"; init( actorA, imageName, App.i().mesS().gwt_stage_LinkTrack_actorTitle() ); actorLink = new ActorLink( actorA, imageName ); new Targeter1( actorA, "actorName" ) { @Override void retarget() { final String p = Stage.i().getActorName(); setTarget( p == null? null: adjustedTarget(App.i().pollwiki().encodePageSpecifier("User:"+p).toString()) ); } }; } /** Adds a tool to the left side of this view. This is only for temporary support of * configuration item {@linkplain votorola.s.gwt.scene.Scenes#toUseRAC() toUseRAC}. */ public @Warning("non-API") void addLeftTool( final Widget tool ) { ((HTMLPanel)getWidget()).add( tool, /*containing element ID*/"LinkTrack-left" ); } /** The link to the {@linkplain votorola.s.wic.diff.WP_D bridge scene} for the staged * {@linkplain Stage#getActorName difference}. */ public ActorLink diffLink() { return diffLink; } private final ActorLink diffLink; @UiField @Warning("non-API") AnchorElement diffA; { final String imageName = "diff"; init( diffA, imageName, App.i().mesS().gwt_stage_LinkTrack_diffTitle() ); diffLink = new ActorLink( diffA, imageName ); if( NominalDifferenceTargeter.isEnabled() ) { new NominalDifferenceTargeter( LinkTrackV.this, diffA ); } else new DifferenceTargeter( LinkTrackV.this, diffA ); } /** The link to the {@linkplain Stage#getActorName actor}'s position draft. */ public ActorLink draftLink() { return draftLink; } private final ActorLink draftLink; @UiField @Warning("non-API") AnchorElement draftA; { final String imageName = "draft"; init( draftA, imageName, App.i().mesS().gwt_stage_LinkTrack_draftTitle() ); draftLink = new ActorLink( draftA, imageName ); new TargeterP( draftA ) { @Override void retarget() { final Stage stage = Stage.i(); final String actorName = stage.getActorName(); final String pollName = stage.getPollName(); if( pollName == null || actorName == null ) setTarget( null ); else { final String contextPersonName; if( draftPageType == DraftPageType.localDraft ) { final PositionID position = App.i().pollwiki().identifyAsPosition( Document.get() ); contextPersonName = position.personName(); if( position != null && actorName.equals(contextPersonName) && pollName.equals(position.pollName()) ) { setTarget( null ); // already on actor's local draft page return; } } else if( draftPageType == DraftPageType.remoteDraft ) { final Document doc = Document.get(); Element marker; marker = doc.getElementById( "voDraft-author" ); if( marker != null ) { contextPersonName = IDPair.normalUsername( marker.getTitle() ); if( actorName.equals( contextPersonName )) { marker = doc.getElementById( "voDraft-poll" ); if( marker != null && pollName.equals( marker.getTitle() )) { setTarget( null ); // already on actor's remote draft page return; } } } else contextPersonName = stage.getDefaultActorName(); } else contextPersonName = stage.getDefaultActorName(); final StringBuilder b = GWTX.stringBuilderClear(); b.append( App.getServletContextLocation() ); b.append( "/w/Draft?p=" ); b.append( pollName.replace( '/', '!' )); b.append( "&u=" ); b.append( URL.encodeQueryString( actorName )); if( !( contextPersonName == null || contextPersonName.equals(actorName) )) { b.append( "&cP=" ); b.append( URL.encodeQueryString( contextPersonName )); } setTarget( adjustedTarget( b.toString() )); } } }; } /** The style variant for actor dependent links, or null if the ordinary style is * used. * * @see #clearActorLinkVariant(String) * @see #setActorLinkVariant(String) */ public String getActorLinkVariant() { return actorLinkVariant; } private String actorLinkVariant; /** Clears the current style variant if it equals the one specified. * * @see #getActorLinkVariant() */ public void clearActorLinkVariant( final String variant ) { if( actorLinkVariant == null || !actorLinkVariant.equals( variant )) return; // clobber guard ElementX.replaceNullClassName( actorLinkVariant, /*new*/null, getWidget().getElement() ); actorLinkVariant = null; syncActorLinkVariant(); } /** Sets the style variant for actor dependent links. A null value forcefully * clears the variant. Consider using {@linkplain #clearActorLinkVariant(String) * clearActorLinkVariant} instead to guard against prematurely clearing what was * set by another caller. * * @see #getActorLinkVariant() * @see ActorLink#resetVariant() */ public void setActorLinkVariant( final String variant ) { ElementX.replaceNullClassName( actorLinkVariant, /*new*/variant, getWidget().getElement() ); /* could OPT by returning if not actually replaced and we knew no resetVariant had been called */ actorLinkVariant = variant; syncActorLinkVariant(); } private void syncActorLinkVariant() { src( actorA, "actor", actorLinkVariant ); src( diffA, "diff", actorLinkVariant ); src( draftA, "draft", actorLinkVariant ); } // - W i d g e t ---------------------------------------------------------------------- protected @Override void onUnload() { super.onUnload(); spool.unwind(); } // ==================================================================================== /** An actor dependent link, the target of which typically depends on the staged * actor. */ public final class ActorLink { private ActorLink( AnchorElement _a, String _imageName ) { a = _a; imageName = _imageName; } private final AnchorElement a; private final String imageName; // -------------------------------------------------------------------------------- /** Forcefully clears the style variant for this actor link. * * @see #getActorLinkVariant() */ public void resetVariant() { src( a, imageName, /*variant*/null ); } } // ==================================================================================== abstract class StageListener extends TheatreInitializer0 implements PropertyChangeHandler { StageListener() { Scheduler.get().scheduleFinally( new Scheduler.ScheduledCommand() { public void execute() { // delayed else init may be called before subclasses constructed Stage.i().addInitializer( StageListener.this ); // auto-removed } }); } /** Called when the stage is completely initialized, adds listeners and * initializes state. * * returns false if init is aborted because the spool is unwinding, true * otherwise. */ boolean init() { if( spool.isUnwinding() ) return false; spool.add( new Hold() { final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/Stage.i(), StageListener.this ); public void release() { hR.removeHandler(); } }); return true; } // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- public void onPropertyChange( PropertyChange _e ) { // GWT BUG (2.4). Method must be defined here in base class, else // NoSuchMethodError may occur on page load (at least in devmode), though that // should be impossible. throw new IllegalStateException(); // always overridden by subclass } // - T h e a t r e - I n i t i a l i z e r ---------------------------------------- public @Override final void initFromComplete( Stage _s, boolean _rPending ) { init(); } public @Override final void initToComplete( Stage _s, boolean _rPending ) { init(); } } // ==================================================================================== /** An agent to adjust the target and enabled state of a link based on one or more * stage properties. */ abstract class Targeter extends StageListener { Targeter( AnchorElement _link ) { link = _link; } @Override boolean init() { if( !super.init() ) return false; retarget(); // init state return true; } // -------------------------------------------------------------------------------- /** Returns the href adjusted by (a) nulling if it matches the current document * location; or (b) insertion of dev mode parameter if dev mode is in effect. */ final String adjustedTarget( final String href ) { if( href == null ) return null; String loc = Document.get().getURL(); final com.google.gwt.regexp.shared.MatchResult devModeLoc; if( GWT.isProdMode() ) devModeLoc = null; else // probably dev mode { devModeLoc = GWTX.DEV_MODE_LOCATION_PATTERN.exec( loc ); if( devModeLoc != null ) // then remove the devmode parameter { final String trailer = devModeLoc.getGroup( 3 ); if( trailer == null || trailer.length() == 0 ) { final String leader = devModeLoc.getGroup( 1 ); loc = leader.substring( 0, leader.length() - 1 ); // chop trailing ?|& } else loc = devModeLoc.getGroup(1) + trailer; } } final String hrefA; // adjusted if( href.equals( loc )) hrefA = null; else { if( devModeLoc == null ) hrefA = href; else if( href.lastIndexOf('#') > 0 ) hrefA = href; // cannot yet handle fragment else { final char delimeter = href.lastIndexOf('?') > 0? '&': '?'; hrefA = href + delimeter + "gwt.codesvr=" + devModeLoc.getGroup( 2 ); } } return hrefA; } private final AnchorElement link; abstract void retarget(); final void setTarget( final String href ) { if( href == null ) { link.addClassName( "disabled" ); link.removeClassName( "enabled" ); link.removeAttribute( "href" ); // actually disable it } else { link.addClassName( "enabled" ); link.removeClassName( "disabled" ); link.setAttribute( "href", href ); } } } // ==================================================================================== /** A targeter that depends on a single stage property. */ abstract class Targeter1 extends Targeter { Targeter1( AnchorElement _link, String _propertyName ) { super( _link ); propertyName = _propertyName; } private final String propertyName; // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- public final void onPropertyChange( final PropertyChange e ) { if( e.propertyName().equals( propertyName )) { retarget(); } } } // ==================================================================================== /** A targeter that depends on multiple stage properties. */ abstract class TargeterN extends Targeter implements Scheduler.ScheduledCommand { TargeterN( AnchorElement _link ) { super( _link ); } final CoalescingSchedulerS coalescer = new CoalescingSchedulerS( CoalescingSchedulerS.FINALLY, TargeterN.this ); // - 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 ------------------------ public final void execute() { retarget(); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// @UiField @Warning("non-API") AnchorElement diffMeA; { init( diffMeA, "diff", App.i().mesS().gwt_stage_LinkTrack_diffMeTitle() ); new TargeterP( diffMeA ) { @Override boolean init() { if( !super.init() ) return false; final TargeterP handler = this; spool.add( new Hold() { final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/App.i(), handler ); public void release() { hR.removeHandler(); } }); return true; } public @Override void onPropertyChange( final PropertyChange e ) { if( e.getSource() == App.i() ) { if( e.propertyName().equals( "username" )) coalescer.schedule(); // which then calls retarget } else super.onPropertyChange( e ); } @Override void retarget() { final String pollName = Stage.i().getPollName(); final String actorName = Stage.i().getActorName(); if( pollName == null || actorName == null || actorName.equals( App.getUsername() )) // avoid self diff { setTarget( null ); } else { setTarget( adjustedTarget( App.getServletContextLocation() + "/w/D?alterAuthor=" + URL.encodeQueryString(actorName) + "&poll=" + URL.encodeQueryString(pollName) )); } } }; } @UiField @Warning("non-API") AnchorElement draftMeA; { init( draftMeA, "draft", App.i().mesS().gwt_stage_LinkTrack_draftMeTitle() ); new TargeterN( draftMeA ) { @Override boolean init() { if( !super.init() ) return false; final TargeterN handler = this; spool.add( new Hold() { final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/App.i(), handler ); public void release() { hR.removeHandler(); } }); return true; } public @Override void onPropertyChange( final PropertyChange e ) { final String name = e.propertyName(); if( e.getSource() == App.i() ) { if( name.equals( "username" )) coalescer.schedule(); // calls retarget } else if( name.equals( "pollName" )) coalescer.schedule(); // calls retarget } @Override void retarget() { final String pollName = Stage.i().getPollName(); if( pollName == null ) setTarget( null ); else { if( draftPageType == DraftPageType.localDraft ) { final String username = App.getUsername(); if( username != null ) { final PositionID position = App.i().pollwiki().identifyAsPosition( Document.get() ); if( position != null && username.equals(position.personName()) && pollName.equals(position.pollName()) ) { setTarget( null ); // already on user's local draft page return; } } } // Else maybe remoteDraft. Cannot currently ID user there. Might // allow username in user CSS along with customization of stage // layout (per s.gwt.mediawiki.MediaWikiIn); but single sign on is // planned, and it's maybe better to wait and see the shape of it. setTarget( adjustedTarget( App.getServletContextLocation() + "/w/MyDraft?p=" + pollName.replace('/','!') )); } } }; } private final DraftPageType draftPageType; private static void init( final AnchorElement a, final String imageName, final String title ) { src( a, imageName, /*variant*/null ); a.setTitle( title ); } @UiField @Warning("non-API") SpanElement leftCaption; private final Text leftText = Document.get().createTextNode( "" ); { leftCaption.appendChild( leftText ); new LeftCaptioner(); } @UiField @Warning("non-API") AnchorElement messageA; { init( messageA, "message", App.i().mesS().gwt_stage_LinkTrack_messageTitle() ); new Targeter1( messageA, "message" ) { @Override void retarget() { final Message m = Stage.i().getMessage(); setTarget( m == null? null: m.location() ); } }; } @UiField @Warning("non-API") AnchorElement pollA; { init( pollA, "poll", App.i().mesS().gwt_stage_LinkTrack_pollTitle() ); new Targeter1( pollA, "pollName" ) { @Override void retarget() { final String p = Stage.i().getPollName(); setTarget( p == null? null: adjustedTarget( App.i().pollwiki().encodePageSpecifier(p).toString()) ); } }; } @UiField @Warning("non-API") SpanElement rightCaption; private final Text rightText = Document.get().createTextNode( "" ); { rightCaption.appendChild( rightText ); new RightCaptioner(); } private static void src( final AnchorElement a, final String imageName, final String variant ) { final ImageElement img = a.getFirstChildElement().cast(); final StringBuilder b = GWTX.stringBuilderClear(); b.append( App.i().staticContextLocation() ); b.append( "/stage/link/" ); b.append( imageName ); if( variant != null ) { assert variant.length() > 0; b.append( '-' ); b.append( variant ); } b.append( ".png" ); img.setAttribute( "src", b.toString() ); } @UiField @Warning("non-API") AnchorElement voteA; { init( voteA, "vote", App.i().mesS().gwt_stage_LinkTrack_voteTitle() ); new TargeterP( voteA ) { @Override void retarget() { final String pollName = Stage.i().getPollName(); if( pollName == null ) setTarget( null ); else { String href = App.getServletContextLocation() + "/w/Votespace?p=" + pollName.replace( '/', '!' ); final String actorName = Stage.i().getActorName(); if( actorName != null ) href += "&u=" + URL.encodeQueryString( actorName ); setTarget( adjustedTarget( href )); } } }; } // ==================================================================================== private static enum DraftPageType { localDraft, nonDraft, remoteDraft } // http://reluk.ca/w/Category:Draft // ==================================================================================== private final class LeftCaptioner extends StageListener { @Override boolean init() { if( !super.init() ) return false; recaption(); // init state return true; } private final @Warning("init call") void recaption() { final Stage s = Stage.i(); final Message m = s.getMessage(); // message takes priority final String c = m == null? s.getPollName(): m.content(); // then poll if( c == null ) leftText.deleteData( 0, leftText.getLength() ); else leftText.setData( c ); } // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- public final void onPropertyChange( final PropertyChange e ) { final String pName = e.propertyName(); if( pName.equals("message") || pName.equals("pollName") ) { recaption(); } } } // ==================================================================================== private final class RightCaptioner extends StageListener { @Override boolean init() { if( !super.init() ) return false; recaption(); // init state return true; } private final @Warning("init call") void recaption() { final String actorName = Stage.i().getActorName(); if( actorName == null ) rightText.deleteData( 0, rightText.getLength() ); else rightText.setData( actorName ); } // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- public final void onPropertyChange( final PropertyChange e ) { if( e.propertyName().equals( "actorName" )) { recaption(); } } } // ==================================================================================== /** A targeter that depends on what position is shown on stage. */ private abstract class TargeterP extends TargeterN { TargeterP( AnchorElement _link ) { super( _link ); } // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- public void onPropertyChange( final PropertyChange e ) { final String pName = e.propertyName(); if( pName.equals("pollName") || pName.equals("actorName") ) coalescer.schedule(); // which then calls retarget } } }