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

import java.util.*;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.net.URL;
import java.net.MalformedURLException;
import java.awt.datatransfer.*;
import java.net.URLDecoder;


import multivalent.*;
import multivalent.gui.VDialog;
import multivalent.std.adaptor.HTML;
import util.Utility;


/**
	This is the familiar point-to-point link.
	Note that it's not built in -- you can add new link types easily.
	Elaborately commented to serve as a simple example of translating Multivalent protocols into Java methods;
	also see {@link multivalent.std.ClipProvenance ClipProvenance}.

	

To do: Make a subclass of ActiveSpan? */ public class HyperlinkSpan extends Span { public final static byte LINK=0, VISITED=1, HOVER=2, ACTIVE=3; /** Target of link can be given as a String or URL. */ protected Object target_ = null; //"(no target)"; /** Flag that records a cache lookup to determine if we've seen this link before, and if so, we can show it in a different color. */ protected boolean seen_=false; // Some behaviors have commented out code, as a reminder to me to implement // sometime approximately like that later or as a warning that although it // seems logical and obvious to do that, it has hidden perils, so don't! // Practically all programmer editors color code source text, so it's easy // to ignore these blocks of comments. //public Color inmediasresColor = Color.red; // => get these from current Layer //public Color underlineColor = Color.blue; // fix this to retrieve from layer //protected boolean inmediasres_ = false; // static? /** Current state of the link--normal, seen, cursor hovering above, mouse clicked on-- show we can show visually. */ protected byte state_ = LINK; /** Record the cursor location when the mouse button is pressed down, so that we can determine if the user moved away to cancel activation, or moved back to reactivate. It's fine to share this field (as static) among links because the user will only be interacting with one link at any one time. */ static protected int x0_,y0_; // only one active hyperlink at a time /** Run-of-the-mill field setter. */ public void setSeen(boolean seen) { //-- compute this during paint? faster startup and time to spare during paint seen_=seen; setState(LINK); } /** Run-of-the-mill field setter. Checks seen_ flag to see if link seen before. */ protected void setState(byte state) { if (state==LINK) state_=(seen_?VISITED:LINK); else state_=state; } /** Run-of-the-mill field getter. */ public byte getState() { return state_; } /** Run-of-the-mill field getter. */ public Object getTarget() { return target_; } /** Run-of-the-mill field getter. Same as getTarget, but more convenient if target type known to be URL. */ public URL getURL() { return (URL)target_; } /** Run-of-the-mill field setter. Targets work best if String or URL types. */ public/*was protected*/ void setTarget(Object o) { target_=o; putAttr("url", o.toString()); } /** Run-of-the-mill field setter. Computes full, canonical URL from a relative specification. The canonical URL is used in the table of links already seen. */ public void setURL(String txt) { target_ = null; if (txt==null) return; try { //URL relto = getBrowser().getURL(); URL relto = getBrowser().getCurDocument().getURL(); URL url = new URL(relto, txt); setTarget(url); //target_ = url; } catch (MalformedURLException e) { // maybe want to cancel link, or show in different color to show it's broken } } /** Spans are ContextListener's, which are behaviors that compose together to determine the Context display properties at every point in the document. For instance, a document will usually determine the font family, size, style, and foreground and background colors, among many other properties, of a piece of text with a combination of ContextListeners reporesenting the influence of style sheet settings, built-in span settings, and perhaps lenses. Here, the generic hyperlink hardcodes the action of coloring the text and gives it an underline, choosing either blue or red. The HTML media adaptor overrides this method to have no action, as the hyperlink appearance is dictated entirely by style sheets, either one linked to the particular web page or failing that the default HTML style sheet. */ public boolean appearance(Context cx, boolean all) { //boolean seen = getControl().seen( cx.foreground = cx.underline = (state_==ACTIVE || target_==null? Color.red: Color.blue); return false; } /** moveq is the low-level, high-speed Span method that removes a Span from one (Node, offset) point and associates it with another. Span subclasses usually don't override it, but we keep a list of all links in the document, and the overrided function makes sure that it is always up to date. */ public void moveq(Node ln,int lo, Node rn,int ro) { Document olddoc = getDocument(); // absolutely do the usual move too super.moveq(ln,lo, rn,ro); Document doc = getDocument(); if (olddoc!=doc) { // null=>valid when add List links; if (olddoc!=null) { links=(List)olddoc.getGlobal("links"); if (links!=null) links.remove(this); } if (doc!=null) { links=(List)doc.getGlobal("links"); if (links!=null) { if (!isSet()) links.remove(this); else if (!links.contains(this)) links.add(this); } } } } /** Restore almost always invokes its superclass, which when it chains up to Behavior sets the behavior's attributes and adds it to the passed layer. Many behaviors also set default parameters and set fields from attributes in the attribute hash table that all behaviors have. See Behavior's superclass, VObject, to examine the various attribute accessor methods. */ public boolean restore(ESISNode n, CHashMap attr, Layer layer) { boolean ret = super.restore(n,attr,layer); target_ = getAttr("url", "(no target)"); // if no attribute "url" exists, default to "(no target)" return ret; } /** On a mouse button down, directly receive all furture low-level events by setting a grab in Browser, until a mouse button up. Also, take the synthesized events (that is, generated by a multivalent.* class as opposed to a java.* class) corresponding to entering and exiting the span, at which time change the cursor and perhaps the colors to indicate that the link is active. As a Span, the hyperlink receives low-level events in the region of the document it spans without additional registering. */ public boolean event(AWTEvent e, Point scrn) { //if (super.event(e,rel)) return true; -- it is dangerous to ignore the superclass, though sometimes necessary; here we're just careless // 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(); MouseEvent me=null; if (eid>=MouseEvent.MOUSE_FIRST && eid<=MouseEvent.MOUSE_LAST) me=(MouseEvent)e; // else return false; -- quick exit, but if later on want to see another event type, this can be confusing // When cursor enters hyperlink region, show by setting cursor to hand, // showing the link destination in the status bar, set state to "hover", // and repaint in case style sheet wants to change color. if (eid==MouseEvent.MOUSE_ENTERED) { br.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); // always show relative link? => make a Preference // If the link type is URL, use a method in the package <tt>util</tt> to // make the link easier to read by showing it relative to the current page // instead of a showing the full link, only the last part of which is usually informative. String showtxt = (target_ instanceof URL? Utility.relativeURL(br.getCurDocument().getURL(), (URL)target_): target_.toString()); setState(HOVER); br.showStatus("Go to "+showtxt); //br.eventq("showStatus", "Go to "+showtxt); // -- showStatus in built-in, but eventually we'll do this by throwing a semantic event repaint(); // When cursor enters hyperlink region, undo the entry actions. } else if (eid==MouseEvent.MOUSE_EXITED) { br.setCursor(Cursor.getDefaultCursor()); setState(LINK); br.showStatus(""); repaint(); // When press button down, set state, redraw link, and grab events until button up. } else if (eid==MouseEvent.MOUSE_PRESSED) { // if not button 1, then exit if ((me.getModifiers()&InputEvent.BUTTON1_MASK)==0) return false; // when the user starts dragging, control is relinquished by the hyperlink, // and a mouse pressed event is generated to signal the default event handlers // to start a selection. This event will first be seen by this hyperlink, // and we'll known that this is the case because the state will still be ACTIVE, // in which case we clean up and don't short-circuit. if (state_==ACTIVE) { setState(LINK); repaint(); return false; } // drag throws a click // set state to currently active, and redraw to usually show link in different color setState(ACTIVE); repaint(); // record cursor location at start of activation x0_=scrn.x; y0_=scrn.y; // set grab to collect all low-level events regardless of cursor position // until grab is released br.setGrab(this); // return true for shortcircuit -- we're handling the mouse click, so short-circuilt // to prevent default event handlers (which would otherwise start a selection) from getting event return true; } else if (eid==MouseEvent.MOUSE_DRAGGED) { // want to turn off inmediasres_, but get MOUSE_DRAG even if don't move mouse // => throw out first n MOUSE_DRAG's // exit existing link => wipe out, set selection & seed edit box // Tolerate a little movement as with some mouses it's hard to click a button // without also moving the mouse and therefore the cursor location. // But if the movement passes a threshold, then assume the user is dragging // out a selection; in that case, relinquish control by the hyperlink // (<tt>releaseGrab</tt>) and construct a mouse pressed event for the default // event handlers. if (Math.abs(scrn.x-x0_)>5 || Math.abs(scrn.y-y0_)>5) { br.releaseGrab(this); // other clean up is done in MOUSE_PRESSED with state already ACTIVE br.event(new MouseEvent((Component)me.getSource(), MouseEvent.MOUSE_PRESSED, me.getWhen()+1, InputEvent.BUTTON1_MASK, x0_,y0_, me.getClickCount(), me.isPopupTrigger())); } return true; // On mouse up, clean up mouse down and invoke go(). } else if (eid==MouseEvent.MOUSE_RELEASED) { if (state_!=ACTIVE) return false; setSeen(true); br.releaseGrab(this); //repaint(0); // clear link appearance -- always on repaint queue? if so, don't repaint portion until next time, at which time it's too late! -- besides, want to see active to mark place go(); // Behaviors that deal with a number of low-level events often end their event() methods this way: // with an <tt>else return false</tt> to pass the event on if the behavior isn't interested, // and a final <tt>return true</tt> to short-circuit when it wants to take exclusive action. } else return false; return true; } /** Override this for special action when hyperlink is clicked. Defaults to sending "openDocument" semantic event to target_. */ public void go() { // seen_=true; -- subclasses don't do a super.go() Browser br = getBrowser(); //System.out.println("HyperlinkSpan "+target_); br.eventq("openDocument", target_); } /** Add to the DOCPOPUP menu--the menu that pops up when the alternative mouse button is clicked over some part of the document (as opposed to the menubar) and the click is not short-circuited out by some behavior. Similarly to ClipProvenance, add "editSpan" if the span is in an editable layer (e.g., if link comes from the HTML sent by a random server, it isn't editable, whereas link annotations you added are), "copyLink", "open in new window", and "open in shared window". */ public boolean semanticEventBefore(SemanticEvent se, String msg) { if (this!=se.getIn()) return false; if ("createWidget/DOCPOPUP"==msg) { INode menu = (INode)se.getOut(); Browser br = getBrowser(); if (isEditable()) { createUI("button", "Edit Link URL", new SemanticEvent(br, "editSpan", this, this, null), menu, "EDIT", false); } createUI("button", "Copy Link to Clipboard", new SemanticEvent(br, "copyLink", this, this, null), menu, "SAVE", false); if (target_ instanceof URL) { CacheInfo ci = new CacheInfo((URL)target_); ci.window = "_NEW"; createUI("button", "Open in New Window", new SemanticEvent(br, "openDocument", ci, null, null), menu, "NAVIGATE", false); ci = new CacheInfo((URL)target_); ci.window = "Aux"; createUI("button", "Open in Shared Window", new SemanticEvent(br, "openDocument", ci, null, null), menu, "NAVIGATE", false); } } return super.semanticEventBefore(se,msg); } /** Catch "copyLink" sent in semanticEventBefore. The pair of "openDocument" are handled by another behavior. Many subclasses have various parameters or attributes, such as URL here or annotation text elsewhere, and the Span class supports editing by catching "editSpan" and throwing up an associated HTML document with a FORM in a note window. When that window is closed, it sends a "data" semantic event with the name-value pairs of the form as a parameter. Hyperlink is interested in the setting of "url". */ public boolean semanticEventAfter(SemanticEvent se, String msg) { if (this!=se.getIn()) return false; // quick exit Object arg=se.getClientData(); //if ("hyperlinkData"==msg) System.out.println("*** processing form in hyperlink 1, in="+in); if ("data"==msg) { // takes data from non-window/non-interactive source too //System.out.println("*** processing form in hyperlink 2"); // process data Map map = (Map)arg; /* for (Iterator i=map.entrySet().iterator(); i.hasNext(); ) { Map.Entry e = (Map.Entry)i.next(); System.out.println(e.getKey()+" = "+e.getValue()); }*/ if (map!=null && map.get("ok")!=null) { // button=OK vs cancel String url = (String)map.get("url"); if (url!=null) { try { setTarget(URLDecoder.decode(url)); } catch (Exception canthappen) {} } else { // alert("must set URL") return true; // keep dialog posted } // if link doesn't already exists, make it Browser br = getBrowser(); if (!isSet()) { Span sel = br.getSelectionSpan(); if (sel.isSet()) move(sel); //else alert("must set selection") } // else maintain current location } // else cancel: don't make / don't edit return true; } else if ("copyLink"==msg) { //System.out.println("copying "+getTarget().toString()+" to clipboard "+arg); StringSelection ss = new StringSelection(getTarget().toString()); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, ss); //getBrowser().setSelection(txt); } return super.semanticEventAfter(se,msg); } public String toString() { return "Hyperlink:"+target_; } }

HyperlinkSpan.java
Created with JBuilder