MoveTextSpan.java
Created with JBuilder
package multivalent.std.span;

import java.util.*;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.AWTEvent;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.KeyEvent;


import multivalent.*;
import util.Location;
import util.Utility;


/**
	Executable copy editor markup that will move marked text to another point in the document.
	Set source text span by making selection and invoking CopyEd / Move Text.
	Set destination point by pressing mouse button down in source text, dragging to end point, and releasing
	(keyboard navigation is still active in case the end point is not on the same screen).
	Source range can be moved in same way as other span annotations, that is, by selecting the new
	destination span and selecting Morph from the span-specific popup menu.
	Destination point can be moved by first removing the current one by invoking
	Re-set move to point from that same menu and setting the new one as before.
	Annotation can subsequently be executed by clicking in the source text.


	

Implementation steps:

  1. Set location of class. (Put in main set of classes but could have put anywhere as long as findable by Java in CLASSPATH.)
  2. UI for setting source text. (Add to Anno menu by adding line to hub via SpanUI in Anno.hub patterned after ReplaceWithSpan.)
  3. Set display for source text, depending on whether not destination point is set. (Model after appearance() from HyperlinkSpan.)
  4. UI in span-specific popup menu. (Add "Re-set move to point" by coping semanticEvent methods from HyperlinkSpan and editing.)
  5. UI for setting destination point. (Model after event() method from HyperlinkSpan.)
  6. Link arrow display to update when an endpoint point changes. (Override Span.moveq() and formatAfter() and catch "formattedDocument" semantic event to recompute display, and Span.remove() to remove destination point as well.)
  7. Display of arrow from source to destination. (Override Behavior.paintAfter().)
  8. Implement move text action. (Model after ActionSpan's action().)
  9. Save and restore annotation to disk. (Extend Span.save() and Span.restore() to include destination point -- superclass already saves range of source text.)
*/ public class MoveTextSpan extends Span { /** Destination point. */ protected Mark moveTo_ = new Mark(null,-1, null); /** Lowest node covering both source and destination. */ protected INode obs_ = null; /** Coordinates of start and end of arrow. */ protected Point sp_=new Point(), ep_=new Point(); protected static boolean active_ = false; protected static boolean skip_=false; /** Add "Re-Set move to point" (event message: resetMoveTo) to span-specific popup menu. */ public boolean semanticEventBefore(SemanticEvent se, String msg) { if (super.semanticEventBefore(se,msg)) return true; else if ("formattedDocument"==msg) setDisplay(); //System.out.println("formattedDoc in MoveText, set="+isSet()+", dest set="+moveTo_.isSet()+", dest="+moveTo_); else if (this!=se.getIn()) return false; else if ("createWidget/DOCPOPUP"==msg) { INode menu = (INode)se.getOut(); Browser br = getBrowser(); if (isEditable()) { if (moveTo_.isSet()) createUI("button", "Re-set move to point", new SemanticEvent(br, "resetMoveTo", this, this, null), menu, "EDIT", false); } } return false; } /** Catch "resetMoveTo" event. */ public boolean semanticEventAfter(SemanticEvent se, String msg) { if (this!=se.getIn()) return false; // quick exit Object arg=se.getClientData(); if ("resetMoveTo"==msg) { moveTo_.remove(); setDisplay(); } return super.semanticEventAfter(se,msg); } public boolean appearance(Context cx, boolean all) { if (moveTo_.isSet()) { cx.underline = Color.red; // paint arrow in paintAfter } else { cx.foreground=Color.white; cx.background=Color.lightGray; } return false; } /** If no destination point, click-drag to set (re-fire keyboard events out so can still scroll). If destination set, execute. */ public boolean event(AWTEvent e, Point scrn) { if (super.event(e,scrn) || skip_) return true; boolean destset = moveTo_.isSet(); // collect up values needed in several places below // even though we're not sure these values will be used, it is not expensive in performance to collect them Browser br = getBrowser(); int eid=e.getID(); // permit keyboard events to scroll document in case destination not on same screen if (eid>=KeyEvent.KEY_FIRST && eid<=KeyEvent.KEY_LAST && br.getGrab()==this) { System.out.println("key in movetext grab"); br.releaseGrab(this); skip_=true; br.event(e); br.setGrab(this); skip_=false; } else if (eid==MouseEvent.MOUSE_ENTERED) { br.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); br.showStatus(destset? "Click to move text": "Choose move to point by pressing down now and dragging to destination."); repaint(); // maybe changed in response to style sheet } else if (eid==MouseEvent.MOUSE_EXITED) { spanExit(); } else if (eid==MouseEvent.MOUSE_PRESSED) { if ((((MouseEvent)e).getModifiers()&InputEvent.BUTTON1_MASK)!=0) { if (destset) { // record cursor location at start of activation //x0_=scrn.x; y0_=scrn.y; // execute on mouse up active_ = true; } br.setGrab(this); // have to set grab during mouse down in order to see mouse up } } else if (eid==MouseEvent.MOUSE_DRAGGED) { //if (br.getGrab()==this) ... // needed? // find point corresponding to current cursor location // (have throw event here to update since grab sends all events directly here) if (active_) { spanExit(); } else { getRoot().eventBeforeAfter(e, scrn); Mark m = br.getCurNode(); Node destn=m.node; int desti=m.offset; if (destn!=null && destn.isLeaf()) { moveTo_.move(m); setDisplay(); // compute x-coordinate within destination // Subsequently made obsolete by new method Leaf.offset2rel(), used in setDisplay //Point rel = destn.getAbsLocation(); //ep_.translate(scrn.x - rel.x, 0); } } } else if (eid==MouseEvent.MOUSE_RELEASED) { if (active_) execute(); spanExit(); } else return false; return true; } /** Several exit points in event(), so collect together. */ protected void spanExit() { Browser br = getBrowser(); br.releaseGrab(this); active_ = false; br.setCursor(Cursor.getDefaultCursor()); br.showStatus(""); repaint(); } /** Set observers. */ protected void setDisplay() { Node sn=getStart().node, dn=moveTo_.node; if (isSet() && moveTo_.isSet() && sn!=dn) { // clean up old settings if (obs_!=null) obs_.deleteObserver(this); // observe on lowest common ancestor obs_ = (INode)sn.commonAncestor(dn); // works but not as efficient: obs_ = getDocument(); obs_.addObserver(this); // compute coordinates (relative to lca since painted in that coordinate space) sp_ = sn.getRelLocation(obs_); ep_ = dn.getRelLocation(obs_); sp_.translate(0, sn.bbox.height/2); ep_.translate(0, dn.bbox.height/2); // center vertically // I see there wasn't an easy way to map a node offset into a visual length, // so it's ok if the horizontal position is set to the start of the word, // but in future map logical offset into horizontal position Point internal = ((Leaf)sn).offset2rel(getStart().offset); sp_.translate(internal.x, internal.y); internal = ((Leaf)dn).offset2rel(moveTo_.offset); ep_.translate(internal.x, internal.y); getBrowser().repaint(); } else { // maybe overwrite source text with message to choose destination point obs_=null; } } public boolean paintAfter(Context cx, Node node) { //System.out.println("paintAfter, node="+node); if (node==obs_) { Graphics2D g = cx.g; g.setColor(Color.red); g.drawLine(sp_.x,sp_.y, ep_.x,ep_.y); } return false; } /** After formatted affected area, recompute coordinates of circle and arrow. */ public boolean formatAfter(Node node) { setDisplay(); return false; } /** Also remove destination point and recompute display (to remove circle and arrow). */ public void removeq() { moveTo_.remove(); if (obs_!=null) obs_.deleteObserver(this); super.removeq(); } /** Also recompute display. */ public void moveq(Node ln,int lo, Node rn,int ro) { super.moveq(ln,lo, rn,ro); setDisplay(); } /** Remove source text, insert at destination point. */ public void execute() { Leaf sn=(Leaf)getStart().node, en=(Leaf)getEnd().node, dn=(Leaf)moveTo_.node; int si=getStart().offset, ei=getEnd().offset, di=moveTo_.offset; remove(); String txt = sn.cut(si, en,ei); dn.insert(di, txt, null); } /** Source text location saved by superclass, so just need to save endpoint (using a Robust Location, of course). */ public void save(StringBuffer sb, int level) { if (moveTo_.isSet()) { // don't save if not valid CHashMap pdest=new CHashMap(5); Location.descriptorFor(moveTo_.node,moveTo_.offset, getDocument(), pdest); // unfortunately, no easy way to add subtree to span save text, so be satisfied for now with an attribute putAttr("DestTree", pdest.get("tree")); //putAttr("DestTree", pdest.get(((Object)"TREE"))); -- required in earlier version used in assignment super.save(sb, level); } } /** Attach destination point as well. */ public void buildAfter(Document doc) { String tree = getAttr("DestTree"); if (tree!=null) { CHashMap pdest = new CHashMap(2); pdest.put("tree", tree); moveTo_.move(Location.attach(pdest, doc)); } super.buildAfter(doc); } }
MoveTextSpan.java
Created with JBuilder