package textbender.a.u.transfer; // Copyright 2006-2007, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Textbender Software"), to deal in the Textbender Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Textbender Software, and to permit persons to whom the Textbender 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 Textbender Software. THE TEXTBENDER 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 TEXTBENDER SOFTWARE OR THE USE OR OTHER DEALINGS IN THE TEXTBENDER SOFTWARE. import java.io.*; import java.rmi.*; import java.util.*; import netscape.javascript.JSObject; import org.w3c.dom.*; import org.w3c.dom.ranges.*; import org.w3c.dom.traversal.*; import textbender.a.r.desk.*; import textbender.a.r.page.*; import textbender.a.r.page.navdo.*; import textbender.d.gene.*; import textbender.g.hold.*; import textbender.g.lang.*; import textbender.g.util.logging.*; import textbender.g.xml.dom.*; import textbender.o.*; import textbender.o.rhinohide.*; import textbender.o.rhinohide._.*; import textbender.o.rhinohide.ranges.*; /** Transferand-loader that responds to text selections in a Web page, * detected via a user-applet. In response, * it loads a transferand corresponding to the text selection. */ public @ThreadSafe final class InPageFactory { /** Constructs an InPageFactory. * * @param vP page visit, context */ public InPageFactory( PageVisit vP ) throws RemoteException { pageVisit = vP; uniqueID = PageDaemons.i().connections().hostServiceRegistry().nextUniqueID(); // documentBody = pageVisit.document().getBody(); new SelectionDetector(); } // ------------------------------------------------------------------------------------ /** The unique identifier of this source. */ public HostServiceRegistry.UniqueID uniqueID() { return uniqueID; } private final HostServiceRegistry.UniqueID uniqueID; //// P r i v a t e /////////////////////////////////////////////////////////////////////// // final Element documentBody; private List leafGeneList( Range range ) { List geneList = Collections.emptyList(); // till proven otherwise if( range.getCollapsed() ) return geneList; range = range.cloneRange(); // avoid modifying caller's final Node root = range.getCommonAncestorContainer(); final TreeWalker walker = ((DocumentTraversal)root.getOwnerDocument()).createTreeWalker ( root, NodeFilter.SHOW_ALL, /*filter*/null, /*expand entities*/false ); Node node; // Trim text not visibly selected by browser (not in inverse video). // This may occur with block genes, such as
, // that have newlines in their trailing/leading whitespace. // Correct by adjusting range to exclude that whitespace. // // And anchor range on text nodes. These will delimit the geneWalk later. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - try { final PassLimiter l = new PassLimiter( 20 ); // avoid hanging, in pathological situations adjustStart: for( ;; l.pass() ) // cf. adjustEnd { trim: for( ;; l.pass() ) { node = range.getStartContainer(); if( !( node instanceof CharacterData )) break trim; if( !( node instanceof Text )) break adjustStart; // avoid problems with comments etc. (per textbender.o.rhinohide.ranges.RhiRange) String selectedText = ((CharacterData)node).substringData ( range.getStartOffset(), Integer.MAX_VALUE ); if( !selectedText.matches( "^\\s*$" )) break adjustStart; // System.out.println( "t.IPF trimming start: [" + ((CharacterData)node).getData() + "]" ); range.setStartAfter( node ); // skip, it's all trailing whitespace if( range.getCollapsed() ) return geneList; } assert !( node instanceof CharacterData ); final int offset = range.getStartOffset(); final NodeList childNodes = node.getChildNodes(); if( offset > 0 ) node = childNodes.item( offset - 1 ); // start from child // else start from parent walker.setCurrentNode( node ); anchor: for( ;; l.pass() ) { node = walker.nextNode(); if( node == null ) return geneList; // no character nodes if( node instanceof Text ) { // System.out.println( "t.IPF anchoring start: [" + ((Text)node).getData() + "]" ); range.setStart( node, /*offset*/0 ); if( range.getCollapsed() ) return geneList; break anchor; } } } // System.out.println( "t.IPF start anchored: [" + ((CharacterData)node).getData() + "]" ); adjustEnd: for( ;; l.pass() ) // cf. adjustStart { trim: for( ;; l.pass() ) { node = range.getEndContainer(); if( !( node instanceof CharacterData )) break trim; if( !( node instanceof Text )) break adjustEnd; // avoid problems with comments etc. (per textbender.o.rhinohide.ranges.RhiRange) String selectedText = ((CharacterData)node).substringData ( 0, range.getEndOffset() ); if( !selectedText.matches( "^\\s*$" )) break adjustEnd; // System.out.println( "t.IPF trimming end: [" + ((CharacterData)node).getData() + "]" ); range.setEndBefore( node ); // skip, it's all leading whitespace if( range.getCollapsed() ) return geneList; } assert !( node instanceof CharacterData ); final int offset = range.getEndOffset(); final NodeList childNodes = node.getChildNodes(); final int childCount = childNodes.getLength(); if( childCount > 0 && offset >= childCount ) // offset past end, start from last descendant { for( ;; l.pass() ) // descend { Node lastChild = node.getLastChild(); if( lastChild == null ) break; node = lastChild; } } else { if( childCount > 0 ) node = childNodes.item( offset ); // start from child // else start from parent walker.setCurrentNode( node ); node = walker.previousNode(); } anchor: for( ;; node = walker.previousNode(), l.pass() ) { if( node == null ) return geneList; // no character nodes if( node instanceof Text ) { // System.out.println( "t.IPF anchoring end: [" + ((Text)node).getData() + "]" ); range.setEnd( node, /*offset*/((Text)node).getLength() ); if( range.getCollapsed() ) return geneList; break anchor; } } } // System.out.println( "t.IPF end anchored: [" + ((CharacterData)node).getData() + "]" ); } catch( PassLimiter.ExceededException x ) { pageVisit.user().logAndShow( x ); return geneList; // empty } // List. Put some genes in it. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - geneList = new ArrayList(); final Node firstNode = range.getStartContainer(); maybePrependGene: for( node = firstNode;; ) // where start node is in a gene { if( node instanceof Element ) { final Element element = (Element)node; if( Gene.isGene( element )) { if( Gene.hasChildGene( element )) break; // first node is (or is in) parent gene geneList.add( element ); // first node is (or is in) leaf gene (that geneWalk would miss, because start of gene precedes range) break; } } node = node.getParentNode(); if( node == null ) break; } final Node lastNode = range.getEndContainer(); walker.setCurrentNode( firstNode ); node = firstNode; geneWalk: for( ;; ) { if( lastNode.isSameNode( node )) break; node = walker.nextNode(); if( node == null ) break; if( !(node instanceof Element )) continue; if( !Gene.isGene( (Element)node )) continue; if( Gene.hasChildGene( node )) continue; // non-leaf geneList.add( (Element)node ); } return geneList; } /** Alternative leafGeneList() based on Range.compareBoundaryPoints(), * that is potentially simpler (but has BUG, q.v. in code). */ private static List leafGeneListCBP( Range range ) { assert false: "code is dead"; List geneList = Collections.emptyList(); // till proven otherwise if( range.getCollapsed() ) return geneList; geneList = new ArrayList(); Node node = range.getCommonAncestorContainer(); final Document document = node.getOwnerDocument(); final Range nodeRange = ((DocumentRange)document).createRange(); final TreeWalker walker = ((DocumentTraversal)document).createTreeWalker ( /*root*/node, NodeFilter.SHOW_ALL, /*filter*/null, /*expand entities*/false ); walkToFirst: for( ;; node = walker.nextNode() ) { if( node == null ) break; if( !(node instanceof Element ) // non-gene || !Gene.isGene( (Element)node ) || Gene.hasChildGene( node )) continue; // non-leaf nodeRange.selectNode( node ); if( range.compareBoundaryPoints(Range.END_TO_START,nodeRange) < 0 ) break; // start of range is before end of node [that's the translation, believe it or not] // // BUG. Misses when only middle part of gene selected. So not using this code... } list: for( ;; node = walker.nextNode() ) { if( node == null ) break; if( !(node instanceof Element ) // non-gene || !Gene.isGene( (Element)node ) || Gene.hasChildGene( node )) continue; // non-leaf nodeRange.selectNode( node ); if( range.compareBoundaryPoints(Range.START_TO_END,nodeRange) <= 0 ) break; // end of range is at/before start of node [that's the translation, believe it or not] geneList.add( (Element)node ); } return geneList; } private final PageVisit pageVisit; private final PRTransferCHub transfer = PageDaemons.i().connections().transferCHub(); private Transferand transferandFrom( final Range range ) { final List leafGeneList = leafGeneList( range ); // OPT List used only because it was already coded. But only the end-genes are needed. It would be faster to calculate them, alone. if( leafGeneList.size() == 0 ) return null; Element gene0 = leafGeneList.get( 0 ); Element gene1 = leafGeneList.get( leafGeneList.size() - 1 ); // FIX promote transferand end-genes to parents return new Transferand ( /*origin*/uniqueID, pageVisit.serializedPage(), Gene.gIndexOf( gene0 ), Gene.gIndexOf( gene1 ) ); } // ==================================================================================== /** Detects changes of text selection, * and loads a new transferand for each. */ private final class SelectionDetector implements Runnable { SelectionDetector() { // polling = pageVisit.rhinohide().eventDispatcher().scheduleAtFixedRate // cannot rely on events, in case pageVisit.isPagePassive(), so poll // ( SelectionDetector.this, /*initial*/4000, 2000, TimeUnit.MILLISECONDS ); //// Runs only once. And then no more events dispatched, either. Anyway, it doesn't matter, this code can no longer be event driven, because it needs to work in slient pages. So: final Thread poller = new Thread( new Runnable() // a thread of my own, for now { public void run() { ThreadX.trySleep( 4000/*ms*/ ); while( !pageVisit.spool().isUnwinding() ) { try { SelectionDetector.this.run(); } catch( StunnedRhinoException xSR ) { LoggerX.i(getClass()).config( Browser.pageExitSupressionMessage( xSR )); } // and keep on detecting, just in case catch( Exception x ) { pageVisit.user().logAndShow( x ); } // and keep on detecting ThreadX.trySleep( 2000/*ms*/ ); } } }, "selection change detector" ); poller.setDaemon( true ); poller.setPriority( Thread.NORM_PRIORITY - 1 ); poller.start(); final JSObject jsoFocusDetector = (JSObject)pageVisit.window().evalV // no longer used, but something like this will soon be needed for 'smoother in-page loader' ( " ( function() { \n" // anon. function literal, to avoid clobbering global namespace with var for new object + " var detector = this; \n" // so it's accessible by event handlers (which are called having event target as their 'this') + " this.isWindowFocus = true; \n" // initially, assume + " this.blurHandler = function( e ) \n" + " { \n" + " detector.isWindowFocus = false; \n" + " }; \n" + " this.focusHandler = function( e ) \n" + " { \n" + " detector.isWindowFocus = true; \n" + " }; \n" + " window.addEventListener( 'blur', this.blurHandler, /*capture*/false ); \n" + " window.addEventListener( 'focus', this.focusHandler, /*capture*/false ); \n" + " return this; \n" + " }).call( new Object() ); \n" // end function literal, and invoke on new object ); focusDetector = new Rhinohide( pageVisit.window(), jsoFocusDetector ); pageVisit.spool().add( new Hold() { public @ThreadSafe void release() { focusDetector.eval ( " window.removeEventListener( 'blur', blurHandler, /*capture*/false ); \n" + " window.removeEventListener( 'focus', focusHandler, /*capture*/false ); \n" ); } }); // Unload transfer before page exit. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The text selection on which the transfer is based naturally disappears // with the containing page. As does pageVisit.serializedPage(), // which the transfer also depends on. So unload it at page exit. // pageVisit.preSpool().add( new Hold() // preSpool, so other components still listening, and can respond to unload, so it takes full effect { public @ThreadSafe void release() { // System.out.println( "t.IPF unload? uniqueID=" + uniqueID ); // polling.cancel( /*interrupt*/false ); final Transferand transferand = transfer.getTransferand(); // System.out.println( "t.IPF unload? transferand=" + transferand ); if( transferand == null ) return; // nothing to unload // System.out.println( "t.IPF unload? transferand.origin()=" + transferand.origin() ); if( !uniqueID.equals( transferand.origin() )) return; // not loaded from here, do not unload // System.out.println( "t.IPF unload? pageVisit.getReleaseReason()=" + pageVisit.getReleaseReason() ); if( VersionedFile.loadAsNewVersion_releaseReason.equals ( pageVisit.getReleaseReason() )) return; // No need to unload transfer in above case. Page exit // will not delete pageVisit.serializedPage(). // Furthermore, the new version may be the result // of an intra-document transfer, where transfer source and target // are the same page. When viewing it, the user may benefit // from the loaded transfer's visual cues (chromography) as feedback. // As well, the user may want to transfer the same object again. try // unload { transfer.setTransferand( null ); } catch( RemoteException x ) { LoggerX.i(getClass()).log( LoggerX.WARNING, /*message*/"", x ); } } }); } private boolean differsFromLast( Range range ) { if( rangeLast == null ) return true; if( range.getCollapsed() && rangeLast.getCollapsed() ) return false; // range still empty return( range.compareBoundaryPoints(Range.END_TO_END,rangeLast) != 0 // more likely to vary than start, so compare first || range.compareBoundaryPoints(Range.START_TO_START,rangeLast) != 0 ); } private final Range emptyRange = ((DocumentRange)pageVisit.document()).createRange(); private final Rhinohide focusDetector; // private final ScheduledFuture polling; private Range rangeLast; // - R u n n a b l e -------------------------------------------------------------- public void run() { Range range; { RhiSelection selection = pageVisit.window().getSelection(); if( selection == null || selection.getRangeCount() == 0 ) range = null; else range = selection.getRangeAt( 0 ); if( range == null ) range = emptyRange; } if( !differsFromLast( range )) { if( !range.getCollapsed() && Boolean.FALSE.equals( focusDetector.getMemberV( "isWindowFocus" ))) { // range.collapse( /*toStart*/true ); // but screen does not refresh in Firefox 2.0.0.1, so try to force: ///// but this messes up Mozilla 1.7.13, leaving selection markings stuck on // InPageStainer.setPageGraphedStyle( documentBody, false ); // ThreadX.trySleep( 100/*ms*/ ); // because it does not always work // InPageStainer.setPageGraphedStyle( documentBody, true ); // if it was not already, it will be soon, so no harm clobbering it like this ///// ugly, should rather set applet/toolbar's
, and gently (commanding it to flash for a second or something, via a threaded refresh() method, documented for that purpose) } return; } // System.out.println( "t.IPF range changed" ); rangeLast = range.cloneRange(); if( range.getCollapsed() ) return; // ignore de-selection // final int[] gIndexArray; // { // List leafGeneList = leafGeneList( range ); // gIndexArray = new int[leafGeneList.size()]; // for( int g = 0; g < gIndexArray.length; ++g ) // { // gIndexArray[g] = Gene.gIndexOf( leafGeneList.get( g )); // } // } // final Transferand tOLast = transfer.getTransferand(); // if( tOLast != null && uniqueID.equals( tOLast.origin() ) // && Arrays.equals( gIndexArray, tOLast.gIndexArray() )) return; // no effective change // // // System.out.println( "t.IPF gene index array changed" ); // try // { // transfer.setTransferand( new Transferand // ( /*origin*/uniqueID, pageVisit.serializedPage(), gIndexArray )); // } // catch( RemoteException x ) { pageVisit.user().logAndShow( x ); } // RemoteException setTransferand final Transferand t = transferandFrom( range ); final Transferand tLast = transfer.getTransferand(); // if( t.equals( tLast ) && uniqueID.equals( tLast.origin() ) return; // no effective change if( t.equals( tLast )) return; // no effective change // System.out.println( "t.IPF setting new transferand" ); try { transfer.setTransferand( t ); } catch( RemoteException x ) { pageVisit.user().logAndShow( x ); } } } }