/*******************************************************************************
* CogTool Copyright Notice and Distribution Terms
* CogTool 1.3, Copyright (c) 2005-2013 Carnegie Mellon University
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* CogTool is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* CogTool is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CogTool; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* CogTool makes use of several third-party components, with the
* following notices:
*
* Eclipse SWT version 3.448
* Eclipse GEF Draw2D version 3.2.1
*
* Unless otherwise indicated, all Content made available by the Eclipse
* Foundation is provided to you under the terms and conditions of the Eclipse
* Public License Version 1.0 ("EPL"). A copy of the EPL is provided with this
* Content and is also available at http://www.eclipse.org/legal/epl-v10.html.
*
* CLISP version 2.38
*
* Copyright (c) Sam Steingold, Bruno Haible 2001-2006
* This software is distributed under the terms of the FSF Gnu Public License.
* See COPYRIGHT file in clisp installation folder for more information.
*
* ACT-R 6.0
*
* Copyright (c) 1998-2007 Dan Bothell, Mike Byrne, Christian Lebiere &
* John R Anderson.
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* Apache Jakarta Commons-Lang 2.1
*
* This product contains software developed by the Apache Software Foundation
* (http://www.apache.org/)
*
* jopt-simple version 1.0
*
* Copyright (c) 2004-2013 Paul R. Holser, Jr.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Mozilla XULRunner 1.9.0.5
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/.
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* The J2SE(TM) Java Runtime Environment version 5.0
*
* Copyright 2009 Sun Microsystems, Inc., 4150
* Network Circle, Santa Clara, California 95054, U.S.A. All
* rights reserved. U.S.
* See the LICENSE file in the jre folder for more information.
******************************************************************************/
package edu.cmu.cs.hcii.cogtool.ui;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.FigureUtilities;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import edu.cmu.cs.hcii.cogtool.model.AAction;
import edu.cmu.cs.hcii.cogtool.model.ButtonAction;
import edu.cmu.cs.hcii.cogtool.model.Design;
import edu.cmu.cs.hcii.cogtool.model.DeviceType;
import edu.cmu.cs.hcii.cogtool.model.DoublePoint;
import edu.cmu.cs.hcii.cogtool.model.DoubleRectangle;
import edu.cmu.cs.hcii.cogtool.model.DoubleSize;
import edu.cmu.cs.hcii.cogtool.model.Frame;
import edu.cmu.cs.hcii.cogtool.model.ContextMenu;
import edu.cmu.cs.hcii.cogtool.model.InputDevice;
import edu.cmu.cs.hcii.cogtool.model.ListItem;
import edu.cmu.cs.hcii.cogtool.model.MenuHeader;
import edu.cmu.cs.hcii.cogtool.model.MenuItem;
import edu.cmu.cs.hcii.cogtool.model.AMenuWidget;
import edu.cmu.cs.hcii.cogtool.model.PullDownHeader;
import edu.cmu.cs.hcii.cogtool.model.Transition;
import edu.cmu.cs.hcii.cogtool.model.IWidget;
import edu.cmu.cs.hcii.cogtool.model.SimpleWidgetGroup;
import edu.cmu.cs.hcii.cogtool.model.KeyAction;
import edu.cmu.cs.hcii.cogtool.model.MouseButtonState;
import edu.cmu.cs.hcii.cogtool.model.MousePressType;
import edu.cmu.cs.hcii.cogtool.model.TapAction;
import edu.cmu.cs.hcii.cogtool.model.TapPressType;
import edu.cmu.cs.hcii.cogtool.model.TextAction;
import edu.cmu.cs.hcii.cogtool.model.WidgetAttributes;
import edu.cmu.cs.hcii.cogtool.model.WidgetType;
import edu.cmu.cs.hcii.cogtool.uimodel.FrameUIModel;
import edu.cmu.cs.hcii.cogtool.util.Cancelable;
import edu.cmu.cs.hcii.cogtool.util.FileUtil;
import edu.cmu.cs.hcii.cogtool.util.KeyboardUtil;
import edu.cmu.cs.hcii.cogtool.util.L10N;
import edu.cmu.cs.hcii.cogtool.util.NamedObjectUtil;
import edu.cmu.cs.hcii.cogtool.util.NullSafe;
import edu.cmu.cs.hcii.cogtool.util.PrecisionUtilities;
import edu.cmu.cs.hcii.cogtool.util.ProgressCallback;
import edu.cmu.cs.hcii.cogtool.util.SWTStringUtil;
import edu.cmu.cs.hcii.cogtool.util.StringUtil;
import edu.cmu.cs.hcii.cogtool.util.WindowUtil;
import edu.cmu.cs.hcii.cogtool.util.GraphicsUtil.ImageException;
/**
* Class which bridges the UIModel and model world for a controller.
* It handles building the VIEWS required for generating HTML pages.
*
* A call exists for building a Frame image. As well as generating the required
* HTML which will provide the "interaction" abilities.
*
* The important thing to remember is that, since a file system places large
* restrictions on file names, only a single call should be made to build the
* webpages. IE: Don't provide an opportunity for the user to
* change names of frames while building an HTML Page.
*
* @author alexeiser
*/
public class DesignExportToHTML
{
public static class ExportIOException extends RuntimeException
{
public ExportIOException(String msg, Throwable cause)
{
super(msg, cause);
}
public ExportIOException(String msg)
{
super(msg);
}
}
public static class ExportException extends RuntimeException
{
public ExportException(String msg, Throwable cause)
{
super(msg, cause);
}
public ExportException(String msg)
{
super(msg);
}
}
// dfm: While exporting a design as HTML shouldn't
// really need to access display level stuff, for expedience the
// display level image rendering code is being used. Unfortunately
// SWT depends upon display level stuff all being done from the main
// thread, but we want to do this in a background thread. To work
// around this problem, we make a few calls here, which will be
// called from the main thread shortly before starting the background
// one, that will initialize enough of SWT's state that the
// background thread stuff still works.
// a) The call to the (now deprecated) method FigureUtilities.getGC()
// has been shown to empirically deal with most of the problems.
// Mike and Jason should be consulted if more information is
// needed about this one.
// b) The second call is to ensure that ColorConstants is initialized
// before the background thread is run, as in some, though not all,
// circumstances values from it will be needed, and its
// initialization depends upon access to the Display.
private static class EnsureGC extends FigureUtilities
{
public static void mustInitializeInMainThread()
{
@SuppressWarnings("deprecation")
GC ignoreGCValue = FigureUtilities.getGC();
// Force the ColorConstants to be initialized. Java is supposed to
// be clever enough to not try to optimize away the following,
// recognizing that accessing a class object potentially has the
// side-effect of loading the class.
Class<ColorConstants> ignoreClassValue = ColorConstants.class;
}
}
/**
* Constants as parameters for the javascript getMouseButton() function
* that makes adjustments for Firefox vs. IE. Values chosen for convenience
* in <code>parseTransitions</code>.
*/
protected static final int LEFT_MOUSE = 1;
protected static final int MIDDLE_MOUSE = 2;
protected static final int RIGHT_MOUSE = 3;
/**
* Flag used in the javascript code. If this string appears as the
* destination URL, it will not display the message below if there are no
* transitions defined.
*/
protected static final String IGNORE_LEFT_CLICK = "'ilc'";
protected static final String NOT_HELP_DEFAULT =
L10N.get("DXH.NotAccomplishGoal",
"This action will not help accomplish your goal");
protected static final String NOT_ACCOMPLISH_GOAL =
"overlib(notAccomplishGoal, STICKY, MOUSEOFF, CENTER, " +
"CLOSETEXT, '', TIMEOUT, 1200, TEXTSIZE, 3);";
protected Map<Frame, String> frameLookUp = new HashMap<Frame, String>();
protected Map<IWidget, String> widgetLookUp = new HashMap<IWidget, String>();
protected Design design;
protected String destDirectory;
protected File parentDir;
/**
* Holds the current suffix to auto insert on new widgets
*/
protected int widgetNameSuffix = 1;
/**
* Maps CogTool's key constants (see {@link KeyAction}) plus other
* non-alphanumeric characters to the Unicode values used in Javascript
*/
protected Map<Character, String> keyCodeMap =
new HashMap<Character, String>();
/**
* if mapOn is true, then we know that we have already started coding up the map tag in html. This variable is only
* used in the "buildWidgetHTML" method.
*/
//private boolean mapOn = false;
/**
* Below two variables is used as temp variables while building html code for all widgets
*/
private String widgetHTML = "";
private String mapHTML = "<map name=\"Button_map\">\n";
/**
* blindHotSpotWarning is a variable that holds information on any warnings that we may want to inform the user.
* If blindHotSpotWarning is not an empty, then this means there is some warning in there, and the user needs to
* be informed about that.
*/
private String blindHotSpotWarning = "";
/**
* This function is the constructor that initiates this class and builds the character/key event handling for Javascript\
*/
public DesignExportToHTML()
{
EnsureGC.mustInitializeInMainThread();
buildKeyCodeMap();
}
protected void buildKeyCodeMap()
{
// Note: no support yet for left/right differences (e.g. shift),
// the Fn key, or function keys above F12
// Second note: backspace, escape, and tab aren't caught by
// Javascript because they do things in the browser
// alt is detected but still gives focus to the menus, so subsequent
// keys might be lost
//Several un-finished implementations
keyCodeMap.put(new Character(KeyboardUtil.SHIFT_CHAR), "\\u0010");
keyCodeMap.put(new Character(KeyboardUtil.CTRL_CHAR), "\\u0011");
//this.keyCodeMap.put(new Character(KeyAction.ALT_CHAR), "\\u0012");
keyCodeMap.put(new Character(KeyboardUtil.COMMAND_CHAR), "\\u00e0");
//this.keyCodeMap.put(new Character(KeyAction.FUNCTION_CHAR), "\\uE004");
//this.keyCodeMap.put(new Character(KeyAction.ESC_CHAR), "\\u001b");
//this.keyCodeMap.put(new Character(KeyAction.BS_CHAR), "\\u0008");
keyCodeMap.put(new Character(KeyboardUtil.CR_CHAR), "\\r");
keyCodeMap.put(new Character(KeyboardUtil.CAPSLOCK_CHAR), "\\u0014");
//this.keyCodeMap.put(new Character(KeyAction.TAB_CHAR), "\\u0009");
keyCodeMap.put(new Character(KeyboardUtil.DEL_CHAR), "\\u002e");
keyCodeMap.put(new Character(KeyboardUtil.MENU_CHAR), "\\u005d");
keyCodeMap.put(new Character(KeyboardUtil.UP_ARROW_CHAR), "\\u0026");
keyCodeMap.put(new Character(KeyboardUtil.DOWN_ARROW_CHAR), "\\u0028");
keyCodeMap.put(new Character(KeyboardUtil.LEFT_ARROW_CHAR), "\\u0025");
keyCodeMap.put(new Character(KeyboardUtil.RIGHT_ARROW_CHAR), "\\u0027");
/*
* Future for function keys
*/
keyCodeMap.put(new Character(KeyboardUtil.F1), "\\u0070");
keyCodeMap.put(new Character(KeyboardUtil.F2), "\\u0071");
keyCodeMap.put(new Character(KeyboardUtil.F3), "\\u0072");
keyCodeMap.put(new Character(KeyboardUtil.F4), "\\u0073");
keyCodeMap.put(new Character(KeyboardUtil.F5), "\\u0074");
keyCodeMap.put(new Character(KeyboardUtil.F6), "\\u0075");
keyCodeMap.put(new Character(KeyboardUtil.F7), "\\u0076");
keyCodeMap.put(new Character(KeyboardUtil.F8), "\\u0077");
keyCodeMap.put(new Character(KeyboardUtil.F9), "\\u0078");
keyCodeMap.put(new Character(KeyboardUtil.F10), "\\u0079");
keyCodeMap.put(new Character(KeyboardUtil.F11), "\\u007a");
keyCodeMap.put(new Character(KeyboardUtil.F12), "\\u007b");
// this.keyCodeMap.put(new Character(KeyAction.F13), "\\uE20D");
// this.keyCodeMap.put(new Character(KeyAction.F14), "\\uE20E");
// this.keyCodeMap.put(new Character(KeyAction.F15), "\\uE20F");
// this.keyCodeMap.put(new Character(KeyAction.F16), "\\uE210");
// Numbers and letters have the same codes in Unicode as those returned
// by Javascript, but almost every other key is different, so every
// non-alphanumeric character that can be typed with one keystroke
// needs to go into the map
keyCodeMap.put(new Character('`'), "\\u00c0");
keyCodeMap.put(new Character('-'), "\\u006d");
keyCodeMap.put(new Character('='), "\\u003d");
keyCodeMap.put(new Character('['), "\\u00db");
keyCodeMap.put(new Character(']'), "\\u00dd");
keyCodeMap.put(new Character('\\'), "\\u00dc");
keyCodeMap.put(new Character(';'), "\\u003b");
keyCodeMap.put(new Character(','), "\\u00bc");
keyCodeMap.put(new Character('.'), "\\u00be");
keyCodeMap.put(new Character('\''), "\\u00de");
keyCodeMap.put(new Character('/'), "\\u00bf");
keyCodeMap.put(new Character(' '), "\\u0020");
}
/**
* The definition of the URL that should be used to access the HTML page
* for this frame. It is the cleaned name of the frame + an extension
*
* @param frame
* @return
*/
protected String getFrameURL(Frame frame, String ext)
{
String result = frameLookUp.get(frame);
try {
result = URLEncoder.encode(result, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new ExportException("Encode of frame page URL failed", e);
}
return result + ext;
}
/**
* The actual file name that refers to the given frame, plus an appropriate
* file extension (.html or .jpg)
*/
protected String getFrameFileName(Frame frame, String ext)
{
return frameLookUp.get(frame) + ext;
}
protected String getWidgetFileName(IWidget widget, String ext)
{
String name = widgetLookUp.get(widget);
if (name == null) {
name = widget.getName().replaceAll(" ", ""); //TODO encode?
widgetLookUp.put(widget, name);
}
return name + ext;
}
public void exportToHTML(Design d,
String dir,
Cancelable cancelState,
ProgressCallback progressState)
{
// No need to duplicate here; we're already in the child thread
// and the design had to have been duplicated in the main thread!
design = d;
destDirectory = dir;
// Performed by the child thread
// Create a file object for DestDir and test if it is actually
// a directory
parentDir = new File(destDirectory);
if (! parentDir.isDirectory()) {
// If there is no directory specified, we should fail here.
throw new IllegalArgumentException("Create Web pages called "
+ "without a directory");
}
Set<Frame> frameSet = design.getFrames();
int frameCount = frameSet.size();
// Start at 0 leaving one extra "count" for overlib copy
// Use "double" to force proper division when setting progress below
double progressCount = 0.0;
// call buildFrameList to build the lookup maps.
buildFrameList();
Iterator<Frame> iter = frameSet.iterator();
ImageLoader imageLoader = new ImageLoader();
String html = null;
Image img = null;
//Go over every frame and create the appropriate file
//Note: Very long while loop in terms of code
while ((! cancelState.isCanceled()) && iter.hasNext()) {
Frame frame = iter.next();
try {
//below function, "buildFrameImage", takes in a frame and returns its image
img = buildFrameImage(frame);
imageLoader.data =
new ImageData[] { img.getImageData() };
String imageName = getFrameFileName(frame, ".jpg");
// create new FILE handler for the new imageSaver's use
File imageFile = new File(parentDir, imageName);
try {
imageLoader.save(imageFile.getCanonicalPath(), SWT.IMAGE_JPEG);
}
catch (IOException ex) {
// We can continue even with exceptions on individual images
throw new ImageException("Failed saving image for HTML export",
ex);
}
}
finally {
// dispose the image, it's not needed any more.
img.dispose();
}
try {
// write HTML to destDir
FileWriter fileOut = null;
BufferedWriter writer = null;
try {
// Use the local file name, and not the complete path.
html = buildFrameHTML(frame);
File htmlFile = new File(parentDir, getFrameFileName(frame, ".html"));
fileOut = new FileWriter(htmlFile);
writer = new BufferedWriter(fileOut);
writer.write(html);
}
finally {
if (writer != null) {
writer.close();
}
else if (fileOut != null) {
fileOut.close();
}
}
}
catch (IOException ex) {
throw new ExportIOException("Could not save HTML for export ",
ex);
}
// Update the progress count
progressCount += 1.0;
progressState.updateProgress(progressCount / frameCount,
SWTStringUtil.insertEllipsis(frame.getName(),
250,
StringUtil.NO_FRONT,
SWTStringUtil.DEFAULT_FONT));
//end of getting frames
}
//make sure user did not cancel, and then create folder "build"
if (! cancelState.isCanceled()) {
File buildDir = new File(parentDir, "build");
if (! buildDir.exists()) {
if (! buildDir.mkdir()) {
throw new ExportIOException("Could not create build directory");
}
}
try {
// Write out the index page.
FileWriter fileOut = null;
BufferedWriter writer = null;
try {
// Use the local file name, and not the complete path.
//This creates the main page, needs a little styling
html = buildIndexPage();
File htmlFile = new File(parentDir, "index.html");
fileOut = new FileWriter(htmlFile);
writer = new BufferedWriter(fileOut);
writer.write(html);
}
finally {
if (writer != null) {
writer.close();
}
else if (fileOut != null) {
fileOut.close();
}
}
}
catch (IOException ex) {
throw new ExportIOException("Could not save index.html for export ",
ex);
}
//Here we import all the resources from the standard directory
InputStream overlibStream =
ClassLoader.getSystemResourceAsStream
("edu/cmu/cs/hcii/cogtool/resources/ExportToHTML/overlib.js");
if (overlibStream == null) {
throw new ExportIOException("Could not locate overlib.js resource");
}
File overlibFile = new File(buildDir, "overlib.js");
InputStream containerCoreStream =
ClassLoader.getSystemResourceAsStream
("edu/cmu/cs/hcii/cogtool/resources/ExportToHTML/container_core.js");
if (containerCoreStream == null) {
throw new ExportIOException("Could not locate container_core.js resource");
}
File containerCoreFile = new File(buildDir, "container_core.js");
InputStream fontsStream =
ClassLoader.getSystemResourceAsStream
("edu/cmu/cs/hcii/cogtool/resources/ExportToHTML/fonts-min.css");
if (fontsStream == null) {
throw new ExportIOException("Could not locate fonts-min.css resource");
}
File fontsFile = new File(buildDir, "fonts-min.css");
InputStream menuStyleStream =
ClassLoader.getSystemResourceAsStream
("edu/cmu/cs/hcii/cogtool/resources/ExportToHTML/menu.css");
if (menuStyleStream == null) {
throw new ExportIOException("Could not locate menu.css resource");
}
File menuStyleFile = new File(buildDir, "menu.css");
InputStream menuStream =
ClassLoader.getSystemResourceAsStream
("edu/cmu/cs/hcii/cogtool/resources/ExportToHTML/menu.js");
if (menuStream == null) {
throw new ExportIOException("Could not locate menu.js resource");
}
File menuFile = new File(buildDir, "menu.js");
//As you can see, lots of yahoo fancy shmancy
InputStream eventStream =
ClassLoader.getSystemResourceAsStream
("edu/cmu/cs/hcii/cogtool/resources/ExportToHTML/yahoo-dom-event.js");
if (eventStream == null) {
throw new ExportIOException("Could not locate yahoo-dom-event.js resource");
}
File eventFile = new File(buildDir, "yahoo-dom-event.js");
InputStream spriteStream =
ClassLoader.getSystemResourceAsStream
("edu/cmu/cs/hcii/cogtool/resources/ExportToHTML/sprite.png");
if (spriteStream == null) {
throw new ExportIOException("Could not locate sprite.png resource");
}
File spriteFile = new File(buildDir, "sprite.png");
try {
FileUtil.copyStreamToFile(overlibStream, overlibFile);
FileUtil.copyStreamToFile(containerCoreStream, containerCoreFile);
FileUtil.copyStreamToFile(fontsStream, fontsFile);
FileUtil.copyStreamToFile(menuStyleStream, menuStyleFile);
FileUtil.copyStreamToFile(menuStream, menuFile);
FileUtil.copyStreamToFile(eventStream, eventFile);
FileUtil.copyStreamToFile(spriteStream, spriteFile);
}
catch (IOException ex) {
throw new ExportIOException("Failed to create file", ex);
}
}
// clear the look up object
frameLookUp.clear();
frameLookUp = null;
}
/**
* Builds the actual image object from the frame.
* Does this by creating an image object the size of the frame, and
* passes it as the canvas for drawing by the FrameDemoUI.
*
* Needs to pass it through a few SWT layers for it to work nicely though.
*
* NOTE: User of this class must dispose of the image themselves
* @param frame
* @return
*/
protected static Image buildFrameImage(Frame frame)
{
// Need the View of the frame before I can build an image for it
FrameUIModel frameUI =
new FrameUIModel(frame,
false,
WindowUtil.SELECT_CURSOR,
1.0,
false,
0, // Don't display color overlays
0,
null); // No attribute override
DoubleSize size = frameUI.getPreferredSize();
// Set a minimum size... this is duplicate code (more or less)
// from DesignUImodel.. but not sure how to take advantage of that
if (size.height < 100) {
size.height = 100;
}
if (size.width < 100) {
size.width = 100;
}
byte[] bgImg = frame.getBackgroundImage();
Image image;
if (bgImg == null) {
image = new Image(null,
PrecisionUtilities.ceiling(size.width),
PrecisionUtilities.ceiling(size.height));
}
else {
image = new Image(null, new ByteArrayInputStream(bgImg));
}
return image;
}
/**
* Builds the actual image object from the frame.
* Does this by creating an image object the size of the frame, and
* passes it as the canvas for drawing by the FrameDemoUI.
*
* Needs to pass it through a few SWT layers for it to work nicely though.
*
* NOTE: User of this class must dispose of the image themselves
* @param frame
* @return
*/
protected static Image buildWidgetImage(IWidget widget)
{
byte[] bgImg = widget.getImage();
if (bgImg == null) {
return null;
}
return new Image(null, new ByteArrayInputStream(bgImg));
}
/**
* Build the map which holds the frame names for a design.
* Changes which design will be used for building HTML pages
*/
protected void buildFrameList()
{
frameLookUp.clear();
Set<String> usedNames = new HashSet<String>();
Iterator<Frame> iter = design.getFrames().iterator();
Random rand = new Random(); // Use a random generator if needed
while (iter.hasNext()) {
Frame frame = iter.next();
String name = cleanStringForFS(frame.getName());
while (usedNames.contains(name)) {
// Name is in use.
// Append a random number.
name = name.concat("+" + rand.nextInt(50));
}
// Now that the name is clean, use it.
frameLookUp.put(frame, name);
usedNames.add(name);
}
}
/**
* Build the HTML around a single frame.
* This really just involves building an image map over the
* areas specified by the widgets, as well as the GraphicalDevices
* Since the graphical Devices don't really have a "Name"
* Also, since the Graphical devices and the frame name are not in the
* Normal frame... (and we are using a frame not a design)
* Need to add graphical devices using normal HTML.
*
* Requires the name of the frame image to be passed in.
*
* A copy of overlib.js needs to be copied to the output directory as well.
* InputStream stream =
* ClassLoader.getSystemResourceAsStream
* ("edu/cmu/cs/hcii/cogtool/resources/overlib.js");
*
* @param frame
* @param frameImageName
* @return
*/
protected String buildFrameHTML(Frame frame)
{
String notHelpString = NOT_HELP_DEFAULT;
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n");
html.append("<html>\n");
html.append("<title>");
html.append(frame.getName());
html.append("</title>\n");
//html.append("<style type='text/css'> body { margin:0; padding:0; } </style>\n");
html.append("<link rel='stylesheet' type='text/css' href='build/fonts-min.css' />\n");
html.append("<link rel='stylesheet' type='text/css' href='build/menu.css' />\n");
html.append("<script type='text/javascript' src='build/yahoo-dom-event.js'></script>\n");
html.append("<script type='text/javascript' src='build/container_core.js'></script>\n");
html.append("<script type='text/javascript' src='build/menu.js'></script>\n");
// Add javascript load for overlay code.
html.append("<script type='text/javascript' ");
html.append(" src='build/overlib.js' ");
html.append(" language='JavaScript'></script>\n");
html.append("<style type='text/css'> div.yuimenu { position: absolute; visibility: hidden; } </style>\n");
html.append("<body style=\"cursor:default;\" class=' yui-skin-sam' onload=\"onLoad()\" onkeydown=\"checkKey(event)\">\n");
// Create the table with the frame name.
html.append("<table border=1>\n");
html.append("<tr>\n");
// -------------------------
// Add the frame title
// -------------------------
html.append("<td align='center'>\n");
html.append(frame.getName());
html.append("</td>\n");
html.append("</tr>\n");
// -------------------------
// Add the device interfaces.
// -------------------------
html.append("<tr>\n");
html.append("<td>\n");
html.append("<img align=center src='");
html.append(getFrameURL(frame, ".jpg"));
//Overlib is a javascript biult-in function that shows a nice pop-up box. DEMONSTRATE!
//I have added a map for this image so that this image will follow a map in the case of "HotSpots"
html.append("' onclick=\"" + NOT_ACCOMPLISH_GOAL + "\" usemap=\"#Button_Map\" />\n");
html.append("</td>\n</tr>\n</table>\n");
// save any menus encountered to create them in javascript later
List<AMenuWidget> menus = new ArrayList<AMenuWidget>();
// only build a group of radio buttons or list box items once
Set<SimpleWidgetGroup> visitedGroups = new HashSet<SimpleWidgetGroup>();
// loop over all widgets
Iterator<IWidget> widgetIterator = frame.getWidgets().iterator();
//NoticS: while function to go over all widgets
while (widgetIterator.hasNext()) {
IWidget widget = widgetIterator.next();
//html.append(buildWidgetHTML(widget, visitedGroups));
widgetHTML += buildWidgetHTML(widget, visitedGroups, frame);
if ((widget instanceof MenuHeader) ||
(widget instanceof ContextMenu))
{
menus.add((AMenuWidget) widget);
}
}
mapHTML += "</map>\n";
html.append(mapHTML);
html.append(widgetHTML);
mapHTML="<map name=\"Button_map\">\n";
widgetHTML="";
//javascript functions
html.append("<script type='text/javascript'>\n");
html.append("var isIE = navigator.appName.indexOf(\"Microsoft\")!=-1;\n");
html.append("var isWin = navigator.appVersion.indexOf(\"Win\")!=-1;\n");
html.append("var notAccomplishGoal = \"" + notHelpString + "\";\n");
html.append("var keyboardString = \"\";\n");
html.append("var hoverElt = null;\n");
html.append("var keyTransitionMap = new Object();\n");
html.append("var clickMap = null;\n");
html.append("var curEvt = null;\n");
html.append("var numClicks = 0;\n");
html.append("var focusString = \"\";\n");
html.append("var focusTransitionMaps = new Object();\n");
html.append("var curFocusMap = null;\n");
int devTypes =
DeviceType.buildDeviceSet(frame.getDesign().getDeviceTypes());
if (DeviceType.Keyboard.isMember(devTypes) ||
DeviceType.Touchscreen.isMember(devTypes))
{
html.append(buildKeyTransitionMap(frame.getInputDevices(),
frame.getWidgets()));
}
// Function to account for IE versus Firefox differences in
// mouse events - they use a different set of mouse button constants
html.append("\nfunction getMouseButton(button)\n{\n");
html.append(" if (button == " + LEFT_MOUSE + ") {\n");
html.append(" return isIE ? 1 : 0;\n }\n");
html.append(" if (button == " + MIDDLE_MOUSE + ") {\n");
html.append(" return isIE ? 4 : 1;\n }\n");
html.append(" if (button == " + RIGHT_MOUSE + ") {\n");
html.append(" return isIE ? 2 : 2;\n }\n}\n\n");
html.append("function onLoad()\n{\n");
html.append(" var str = document.location.search;\n");
html.append(" if (str != null) {\n");
html.append(" str = str.substr(1);\n");
html.append(" var search = str.split(\"&\");\n");
html.append(" var searchCount = search.length;\n");
html.append(" for (i = 0; i < searchCount; i++) {\n");
html.append(" var check = search[0].split(\"=\");\n");
html.append(" if ((check.length == 2) && (check[0] == \"focusID\")) {\n");
html.append(" var elt = document.getElementById(decodeURIComponent(check[1]));\n");
html.append(" if (elt != null) {\n");
html.append(" elt.focus();\n");
html.append(" if (elt.type == 'radio') {\n");
html.append(" elt.checked = true;\n }\n");
html.append(" else if (elt.type == 'checkbox') {\n");
html.append(" elt.checked = ! elt.checked;\n");
html.append(" }\n");
html.append(" }\n");
html.append(" break;\n");
html.append(" }\n }\n }\n} // onLoad\n\n");
// The following two methods are only used for Yahoo menu events.
// newframe is a string representing the html file of the frame to be
// transitioned to; button is the mouse button that causes that
// transition, and e is the actual event that is checked against
html.append("function checkMenuTransitions(newFrame, button, e)\n{\n");
html.append(" return (newFrame && (button == e.button));\n}\n\n");
// The Yahoo menus use this function, so it requires three parameters.
// The first we don't use, the second stores the event, and the third
// is an array of strings representing html files. The function above
// takes this information plus a mapping from the index in that array
// to the mouse button that effects the corresponding transition, and
// determines whether a transition should occur. If so, it changes
// the location of the document.
html.append("function menuTransition(ignore, p_aArgs, p_oValue)\n{\n");
html.append(" var transitionIndex = -1;\n for (i = 0; i < p_oValue.length; i++) {\n");
html.append(" if (checkMenuTransitions(p_oValue[i], getMouseButton(i+1), p_aArgs[0])) {\n");
html.append(" if ((p_oValue[i] == " + IGNORE_LEFT_CLICK + ")) {\n");
html.append(" return;\n }\n");
html.append(" transitionIndex = i;\n }\n }\n");
html.append(" if (transitionIndex >= 0) {\n");
html.append(" document.location.href = p_oValue[transitionIndex];\n }\n");
html.append(" else {\n " + NOT_ACCOMPLISH_GOAL + "\n }\n}\n\n");
// Return an integer representing the modifier state of the current
// event object (NOTE: event has "metaKey" but no fnKey or cmdKey)
html.append("function getModifiers(e)\n{\n");
html.append(" var result = 0;\n if (e.shiftKey) {\n");
html.append(" result += " + AAction.SHIFT + ";\n }\n");
html.append(" if (e.ctrlKey) {\n result += " + AAction.CTRL + ";\n }\n");
html.append(" if (e.altKey) {\n result += " + AAction.ALT + ";\n }\n\n");
html.append(" return result;\n}\n\n");
// The following two functions are used for transitions from regular
// HTML elements. This one takes the current event object, the set
// of values to be considered, and the number of clicks that have
// occurred and determines whether a transition should be followed.
html.append("function checkTransitions(clicks, params, event)\n{\n");
html.append(" if (clicks != params[1]) {\n return false;\n }\n\n");
html.append(" var modifiers = getModifiers(event);\n");
html.append(" var button = event.button;\n");
html.append(" if ((params[0] == " + RIGHT_MOUSE + ") &&");
html.append(" (modifiers & " + AAction.CTRL + ") && (! isWin)) {\n");
html.append(" button = getMouseButton(" + RIGHT_MOUSE + ");\n");
html.append(" modifiers -= " + AAction.CTRL + ";\n }\n\n");
html.append(" if (getMouseButton(params[0]) != button) {\n");
html.append(" return false;\n }\n if (modifiers != params[2]) {\n");
html.append(" return false;\n }\n\n return true;\n}\n\n");
// clicks is the number of clicks performed on the widget (1, 2, or 3),
// and event is the HTML event object. transitionMap is an array of
// arrays. The sub-arrays have four elements that together define a
// mouse transition: which button, how many clicks, the keyboard
// modifier state, and the destination frame, in that order.
html.append("function followTransition(clicks, event, transitionMap)\n{\n");
html.append(" var frame = null;\n");
html.append(" for (i = 0; i < transitionMap.length; i++) {\n");
html.append(" var params = transitionMap[i];\n");
html.append(" if (checkTransitions(clicks, params, event)) {\n");
html.append(" frame = params[3];\n");
html.append(" if ((frame == " + IGNORE_LEFT_CLICK + ") && (clicks == 1)) return;\n }\n }\n\n");
html.append(" if (frame != null) {\n");
html.append(" document.location.href = frame;\n");
html.append(" return true;\n }\n");
html.append(" " + NOT_ACCOMPLISH_GOAL + "\n return false;\n}\n\n");
// Used for context menus to put the menu where the mouse was clicked
html.append("function displayContextMenu(event, menu)\n{\n");
html.append(" menu.cfg.setProperty(\"x\", event.clientX);\n");
html.append(" menu.cfg.setProperty(\"y\", event.clientY);\n");
html.append(" menu.show();\n}\n\n");
// This function allows clicking on the label of a checkbox to toggle
// it instead of only being able to click on the box itself. CogTool
// checkbox widgets work this way.
html.append("function toggleCheckbox(eltId)\n{\n");
html.append(" var elt = document.getElementById(eltId);\n");
html.append(" if (typeof(elt) != 'undefined') {\n");
html.append(" if (elt.type == 'checkbox') {\n");
html.append(" elt.checked = ! elt.checked;\n }\n }\n}\n");
// Similar to toggleCheckbox, but for radio buttons
html.append("function selectRadio(eltId)\n{\n");
html.append(" var elt = document.getElementById(eltId);\n");
html.append(" if (typeof(elt) != 'undefined') {\n");
html.append(" if (elt.type == 'radio') {\n");
html.append(" elt.checked = true;\n }\n }\n}\n\n");
// There is no notion of "hover" in html, only mouse over. So if
// there is a hover transition, it needs to save the frame to be
// transitioned to in a global variable and start a timer to see
// if the cursor remains hovered over the element.
html.append("function timeHover(frame)\n{\n hoverElt = frame;\n");
html.append(" window.setTimeout(\"checkHover()\", 1000);\n}\n\n");
// When the timer is up, if the cursor is still there, it does the
// transition.
html.append("function checkHover()\n{\n if (hoverElt) {\n");
html.append(" document.location.href = hoverElt;\n }\n");
html.append(/*else {\n" + NOT_ACCOMPLISH_GOAL + "\n}\n*/"}\n\n");
// If there is a mouse out event on any widget, it discards the hover
// information.
html.append("function cancelHover()\n{\n hoverElt = null;\n}\n\n");
// Used for keyboard transitions. Taking into account regexp special
// characters, it uses a regular expression to test whether the string
// str (the string that collects keyboard input) ends with the string
// s (the string that causes a transition)
html.append("function endsWith(str, s)\n{\n");
html.append(" s = s.replace(/\\\\|\\(|\\)|\\*|\\^|\\$|\\+|\\?|\\.|\\||\\{|\\}|\\[|\\]/g, escaper);\n");
html.append(" var reg = new RegExp(s + \"$\");\n");
html.append(" return reg.test(str);\n}\n\n");
// Called when a key is pressed; adds the new key to the keyboard
// string and, if applicable, the string for the widget that has the
// focus (first), and checks whether it should do a transition
// TODO NOTE: Widgets for which key transitions don't work:
// radio buttons, links, menu items, pull-down items, list box items
html.append("function checkKey(e)\n{\n if (curFocusMap) {\n");
html.append(" focusString += String.fromCharCode(e.keyCode);\n");
html.append(" var result = checkStrings(focusString, curFocusMap);\n");
html.append(" if (result) {\n document.location.href = result;\n");
html.append(" return;\n }\n }\n");
html.append(" keyboardString += String.fromCharCode(e.keyCode);\n");
html.append(" var result = checkStrings(keyboardString, keyTransitionMap);\n");
html.append(" if (result) {\n document.location.href = result;\n }\n}\n\n");
// Returns the longest string that matches the currently collected
// input, or null
html.append("function checkStrings(collector, map)\n{\n");
html.append(" var longest = null;\n");
html.append(" for (var i in map) {\n");
html.append(" if (endsWith(collector.toLowerCase(), i)) {\n");
html.append(" if (! longest) {\n longest = i;\n }\n");
html.append(" if (i.length > longest.length) {\n longest = i;\n");
html.append(" }\n }\n }\n");
html.append(" if (longest) {\n return map[longest];\n }\n");
html.append(" return null;\n}\n\n");
// adds escape characters to special keys so the regexp doesn't
// get confused
html.append("function escaper(c)\n{\n return \'\\\\\' + c;\n}\n\n");
// html detects left double-clicks but not middle or right double clicks,
// so the next two functions handle that. This one is called on mouse
// up (onclick doesn't detect right clicks, see buildWidgetHTML()).
// transitionMap is an array of arrays (see the comments above
// followTransition). It starts a timer after it is called; if
// it is called again before the timer runs out (as determined by the
// state of the global numClicks variable), it starts another timer,
// otherwise it checks a single click transition. If the second timer
// runs out, it checks a double click transition, but if this function
// is called a third time, it checks a triple click transition.
// TODO: NOTE: Even with onmouseup, html seems to fail to detect middle
// clicks in Firefox, so this solution works only with right clicks.
html.append("function checkClicks(transitionMap, e)\n{\n");
html.append(" curEvt = e;\n numClicks++;\n\n");
html.append(" if (numClicks == 3) {\n");
html.append(" if (followTransition(3, e, transitionMap)) {\n");
html.append(" window.clearTimeout();\n }\n\n");
html.append(" numClicks = 0;\n }\n else {\n");
html.append(" clickMap = transitionMap;\n");
html.append(" if (numClicks == 1) {\n");
html.append(" window.setTimeout(\"doClick1()\", 500);\n }\n");
html.append(" else if (numClicks == 2) {\n");
html.append(" window.clearTimeout();\n");
html.append(" window.setTimeout(\"doClick2()\", 300);\n }\n }\n}\n\n");
// If there was only a single click, it uses the saved
// map to try to perform a transition with one click, and clears the state.
html.append("function doClick1()\n{\n");
html.append(" if ((clickMap != null) && (numClicks == 1)) {\n");
html.append(" followTransition(1, curEvt, clickMap);\n");
html.append(" clickMap = null;\n numClicks = 0;\n }\n}\n\n");
// If there was a double click, it uses the saved
// map to try to perform a transition with two clicks, and clears the state.
html.append("function doClick2()\n{\n");
html.append(" if ((clickMap != null) && (numClicks == 2)) {\n");
html.append(" followTransition(2, curEvt, clickMap);\n");
html.append(" clickMap = null;\n numClicks = 0;\n }\n}\n\n");
// Called when any widget gets the focus (html's onfocus). Uses the
// widgets ID to index into the map of widgets' keyboard transitions.
html.append("function onFocus(eltID)\n{\n");
html.append(" keyboardString = \"\";\n");
html.append(" var map = focusTransitionMaps[eltID];\n");
html.append(" if (typeof(map) != 'undefined') {\n");
html.append(" curFocusMap = focusTransitionMaps[eltID];\n }\n}\n\n");
// Clears the current focus map so it knows that no widget has the focus
html.append("function onBlur()\n{\n curFocusMap = null;\n}\n\n");
html.append("YAHOO.util.Event.onDOMReady(function () {\n");
// use yahoo javascript library to build up menus
if (menus.size() > 0) {
for (int i = 0; i < menus.size(); i++) {
AMenuWidget menu = menus.get(i);
String menuVar = "oMenu" + i;
String context = "";
if (menu instanceof MenuHeader) {
// associate it with the corresponding menu header
context = ", { context: [\"" + menu.getName() + "\", \"tl\", \"bl\"]}";
}
// else, if it is a context menu, have it appear wherever the
// mouse is clicked (in a javascript method)
// create the javascript menu object
html.append(" var " + menuVar + " = new YAHOO.widget.Menu(\"menu" +
i + "\"" + context + ");\n");
html.append(" " + menuVar + ".addItems(" +
buildItemsString(menu.getChildren().iterator()) +
");\n");
html.append(" " + menuVar + ".showEvent.subscribe(function () { this.focus(); });\n");
html.append(" " + menuVar + ".render(document.body);\n");
if (menu instanceof MenuHeader) {
html.append(" YAHOO.util.Event.addListener(\"" + menu.getName() +
"\", \"click\", " + menuVar + ".show, null, " + menuVar + ");\n");
}
else if (menu instanceof ContextMenu) {
html.append(" YAHOO.util.Event.addListener(\"" + menu.getName() +
"\", \"contextmenu\", displayContextMenu, " + menuVar + ", " + menuVar + ");\n");
}
}
}
html.append("\n document.body.oncontextmenu = function () {\n return false;\n }\n});\n\n");
html.append("</script>\n");
html.append("</body>\n");
html.append("</html>\n");
return html.toString();
}
/**
* Outputs a Javascript function that makes a map from the transition
* string to the name of the destination frame
* (treats Graffiti transitions as keyboard transitions)
*/
protected String buildKeyTransitionMap(Collection<InputDevice> devices,
Collection<IWidget> widgets)
{
StringBuilder html = new StringBuilder();
Iterator<InputDevice> deviceIterator = devices.iterator();
while (deviceIterator.hasNext()) {
InputDevice device = deviceIterator.next();
if (DeviceType.Keyboard.equals(device.getDeviceType()) ||
DeviceType.Touchscreen.equals(device.getDeviceType()))
{
// first save all transitions from the device
Iterator<Transition> transitions =
device.getTransitions().values().iterator();
while (transitions.hasNext()) {
Transition transition = transitions.next();
AAction action = transition.getAction();
if (action instanceof TextAction) {
String text = cleanup(((TextAction) action).getText());
html.append("keyTransitionMap[\"" + text + "\"]");
html.append(" = \'");
html.append(getFrameURL(transition.getDestination(), ".html"));
html.append("\';\n");
}
}
}
}
Iterator<IWidget> widgetIterator = widgets.iterator();
// go through every widget and record its keyboard or graffiti transitions
// NOTE: does not work with list box items or any menus
while (widgetIterator.hasNext()) {
IWidget w = widgetIterator.next();
String name = "\"" + w.getName() + "\"";
Iterator<Transition> wTransitions =
w.getTransitions().values().iterator();
boolean foundKeyTransition = false;
while (wTransitions.hasNext()) {
Transition transition = wTransitions.next();
AAction action = transition.getAction();
if (action instanceof TextAction) {
if (! foundKeyTransition) {
foundKeyTransition = true;
html.append("focusTransitionMaps[" + name + "] = new Object();\n");
}
String key = "\"" + cleanup(((TextAction) action).getText()) + "\"";
String dest = "'" + getFrameURL(transition.getDestination(), ".html") + "'";
html.append("focusTransitionMaps[" + name + "][");
html.append(key + "] = " + dest + ";\n");
}
}
}
return html.toString();
}
/**
* see {@link KeyboardUtil} and <code>buildKeyCodeMap()</code>
*/
protected String cleanup(String text)
{
StringBuilder result = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
String s1 = KeyboardUtil.convertToKLM(c);
for (int j = 0; j < s1.length(); j++) {
char subChar = s1.charAt(j);
if (Character.isLetterOrDigit(subChar)) {
result.append(subChar);
}
else {
Character key = new Character(subChar);
if (keyCodeMap.containsKey(key)) {
result.append(keyCodeMap.get(key));
}
else {
// TODO: error?
System.err.println("Unrecognized character: " + subChar);
}
}
}
}
return result.toString();
}
protected String buildItemsString(Iterator<IWidget> children)
{
StringBuilder itemString = new StringBuilder();
itemString.append("[\n");
while (children.hasNext()) {
MenuItem child = (MenuItem) children.next();
itemString.append(" { text: \"" + child.getTitle() + "\"");
if (child.isSubmenu()) {
itemString.append(", submenu: { id: \"" + child.getName() +
"\", itemdata: " +
buildItemsString(child.getChildren().iterator()) +
"} ");
}
else {
itemString.append(", onclick: { fn: menuTransition, obj: ");
String menuMap = getMenuMap(child);
itemString.append(menuMap + "\n}\n");
}
itemString.append("},\n");
}
itemString.append(" ]");
return itemString.toString();
}
/**
* Return an array of three strings, corresponding to the three possible
* mouse buttons, representing either the name of the frame transitioned to
* by that button, or 'null'.
* NOTE: Hover and multiple-click transitions don't work with menu items.
*/
protected String getMenuMap(MenuItem child)
{
String result = "[";
// TODO: onmouseup doesn't work for middle clicks...except
// sometimes it does, may be related to focus?
// Always use onmouseup instead of onclick because we need to check
// for double click here too
String[] mouseUpDests = new String[4];
mouseUpDests[0] = "null";
int mouseUpButton = 0;
Iterator<Transition> transitions = child.getTransitions().values().iterator();
while (transitions.hasNext()) {
Transition transition = transitions.next();
AAction action = transition.getAction();
if (action instanceof ButtonAction) {
ButtonAction bAction = (ButtonAction) action;
MouseButtonState buttonState = bAction.getButton();
MousePressType pressType = bAction.getPressType();
int button = -1;
String destName = getFrameURL(transition.getDestination(), ".html");
if (! MousePressType.Hover.equals(pressType)) {
if (MouseButtonState.Left.equals(buttonState)) {
button = LEFT_MOUSE;
}
else if (MouseButtonState.Middle.equals(buttonState)) {
button = MIDDLE_MOUSE;
}
else if (MouseButtonState.Right.equals(buttonState)) {
button = RIGHT_MOUSE;
}
/*if (MousePressType.Click.equals(pressType) &&
(button == LEFT_MOUSE))
{
//onAction = "onclick";
// clickDest = "'" + destName + "'";
mouseUpButton = button;
mouseUpDests[mouseUpButton] = "'" + destName + "'";
}
else */if (MousePressType.Up.equals(pressType) ||
MousePressType.Click.equals(pressType))
{
//onAction = "onmouseup";
mouseUpButton = button;
mouseUpDests[mouseUpButton] = "'" + destName + "'";
}
// else if (MousePressType.Double.equals(pressType)) {
// //onAction = "ondblclick";
// dblClickButton = button;
// dblClickDests[dblClickButton] = "'" + destName + "'";
// }
// else if (MousePressType.Down.equals(pressType)) {
// onAction = "onmousedown";
// }
}
}
else if (action instanceof TapAction) {
TapAction bAction = (TapAction) action;
TapPressType pressType = bAction.getTapPressType();
String destName = getFrameURL(transition.getDestination(), ".html");
if (! TapPressType.Hover.equals(pressType)) {
if (TapPressType.Tap.equals(pressType))
{
// TODO: Behavior is undefined if both a left-click and
// tap transition are created from the same widget!
//onAction = "onclick";
// clickDest = "'" + destName + "'";
mouseUpButton = LEFT_MOUSE;
mouseUpDests[mouseUpButton] = "'" + destName + "'";
}
// else if (TapPressType.Up.equals(pressType)) {
// onAction = "onmouseup";
// }
// else if (TapPressType.DoubleTap.equals(pressType)) {
// //onAction = "ondblclick";
// // TODO see tap
// dblClickButton = LEFT_MOUSE;
// dblClickDests[dblClickButton] = "'" + destName + "'";
// }
// else if (TapPressType.Down.equals(pressType)) {
// //onAction = "onmousedown";
// }
}
}
}
for (int i = 1; i < mouseUpDests.length; i++) {
if (mouseUpDests[i] == null) {
mouseUpDests[i] = "null";
}
// if (dblClickDests[i] == null) {
// dblClickDests[i] = "null";
// }
result += mouseUpDests[i];
if (i < 3) {
result += ", ";
}
}
result += "]";
return result;
}
/**
* Given the transitions from a widget, parse them and return 3 strings:
* first, the name of the destination frame of its hover transition;
* second, the map that defines its single click transitions;
* third, the map that defines its double click transitions.
* (See comments on the javascript functions for how these maps are used.)
*/
protected String[] parseTransitions(IWidget w,
boolean checkFocus,
int ignoreButton)
{
String[] result = new String[2];
result[0] = "null";
result[1] = "[";
// TODO: onmouseup doesn't work for middle clicks...except
// sometimes it does, may be related to focus?
// Always use onmouseup instead of onclick because we need to check
// for double click here too
Iterator<Transition> transitions = w.getTransitions().values().iterator();
// If we are told to ignore transitions from a certain button, check
// if the widget has any transitions defined from that button. If not,
// tell the javascript explicitly not to pop up the "does not help..."
// message if that button is clicked with the IGNORE_LEFT_CLICK string
// flag.
boolean hasIgnoredButtonTransition = false;
while (transitions.hasNext()) {
Transition transition = transitions.next();
AAction action = transition.getAction();
if (action instanceof ButtonAction) {
ButtonAction bAction = (ButtonAction) action;
MouseButtonState buttonState = bAction.getButton();
MousePressType pressType = bAction.getPressType();
int button = -1;
String destName = getFrameURL(transition.getDestination(), ".html");
int modifiers = bAction.getModifiers();
int numClicks = 0;
if (MousePressType.Hover.equals(pressType)) {
result[0] = "'" + destName + "'";
}
else {
if (MouseButtonState.Left.equals(buttonState)) {
button = LEFT_MOUSE;
if (checkFocus) {
destName += buildFocusSearch(w,
transition.getDestination());
}
}
else if (MouseButtonState.Middle.equals(buttonState)) {
button = MIDDLE_MOUSE;
}
else if (MouseButtonState.Right.equals(buttonState)) {
button = RIGHT_MOUSE;
}
if (MousePressType.Up.equals(pressType) ||
MousePressType.Click.equals(pressType))
{
numClicks = 1;
if ((button == ignoreButton) &&
(modifiers == AAction.NONE))
{
hasIgnoredButtonTransition = true;
}
}
else if (MousePressType.Double.equals(pressType)) {
numClicks = 2;
}
else if (MousePressType.Triple.equals(pressType)) {
numClicks = 3;
}
if (result[1].length() > 1) {
result[1] += ", ";
}
result[1] += "[" + button + ", " + numClicks + ", " +
modifiers + ", '" + destName + "']";
}
}
else if (action instanceof TapAction) {
TapAction bAction = (TapAction) action;
TapPressType pressType = bAction.getTapPressType();
String destName = getFrameURL(transition.getDestination(), ".html");
int numClicks = 0;
if (TapPressType.Hover.equals(pressType)) {
result[0] = "'" + destName + "'";
}
else {
if (TapPressType.Up.equals(pressType) ||
TapPressType.Tap.equals(pressType))
{
// TODO: Behavior is undefined if both a left-click and
// tap transition are created from the same widget!
numClicks = 1;
if (ignoreButton == 1) {
hasIgnoredButtonTransition = true;
}
}
// else if (TapPressType.Up.equals(pressType)) {
// onAction = "onmouseup";
// }
else if (TapPressType.DoubleTap.equals(pressType)) {
//onAction = "ondblclick";
// TODO see tap
numClicks = 2;
}
else if (TapPressType.TripleTap.equals(pressType)) {
// TODO see tap
numClicks = 3;
}
if (result[1].length() > 1) {
result[1] += ", ";
}
result[1] += "[" + LEFT_MOUSE + ", " + numClicks + ", " +
AAction.NONE + ", '" + destName + "']";
}
}
}
if (! hasIgnoredButtonTransition) {
// We want to ignore clicks if there isn't a click
// transition already defined
if (result[1].length() > 1) {
result[1] += ", ";
}
result[1] += "[" + ignoreButton + ", 1, 0, " + IGNORE_LEFT_CLICK + "]";
}
result[1] += "]";
return result;
}
protected String getEventString(IWidget widget, int ignoreButton)
{
String[] strings = parseTransitions(widget, true, ignoreButton);
//time hover is a javascript function that I think is predefined, TO DO: make sure about that
String eventString = " onmouseover=\"timeHover(" + strings[0] +
")\" onmouseout=\"cancelHover()\"";
// eventString += " onclick" + "=\"transition(null, [event], " +
// "[[" + clickDest + "], [" + LEFT_MOUSE + "]])\"";
eventString += " onmouseup" + "=\"checkClicks(" +
strings[1] + ", event)\"";
// eventString += " ondblclick" + "=\"transition(null, [event], " +
// "[[" + dblClickDests[LEFT_MOUSE] +
// "], [" + LEFT_MOUSE + "]])\"";
return eventString;
}
/**
* Checks whether a copy of the given widget exists in the destination
* frame; if so, return the information to the html in a search string.
*/
protected String buildFocusSearch(IWidget widget, Frame destination)
{
String search = "";
Iterator<IWidget> widgets = destination.getWidgets().iterator();
while (widgets.hasNext()) {
IWidget w = widgets.next();
if (w.isIdentical(widget)) {
// TODO: is this the right test?
search = "?focusID=" + w.getName();
}
}
return search;
}
/**
* For checkboxes and radio buttons, clicking on the text should cause
* the widget to change selection. The exception to this is when the
* widget already has a left-click transition defined that is not a
* self-transition, since then it would override the onclick property.
* @param widget
* @return
*/
protected boolean textSelect(IWidget widget)
{
Iterator<Transition> transitions =
widget.getTransitions().values().iterator();
boolean leftClick = false;
while (transitions.hasNext()) {
Transition transition = transitions.next();
AAction action = transition.getAction();
if (action instanceof ButtonAction) {
ButtonAction bAction = (ButtonAction) action;
MouseButtonState buttonState = bAction.getButton();
MousePressType pressType = bAction.getPressType();
if (MouseButtonState.Left.equals(buttonState) &&
MousePressType.Click.equals(pressType))
{
leftClick = true;
if (transition.getDestination().equals(widget.getFrame()))
{
return true;
}
}
}
}
if (leftClick) {
return false;
}
return true;
}
protected String buildWidgetHTML(IWidget widget,
Set<SimpleWidgetGroup> visitedGroups,
Frame frame)
{
//Function is called once for each widget...
StringBuilder html = new StringBuilder();
DoubleRectangle bounds = widget.getEltBounds();
String name = widget.getName();
//How does below boolean work?
boolean isStandard = widget.isStandard();
//MAYBE CREATE AN IF STATEMENT TO HANDLE BUTTONS/MENU BUTTON DIFFERENTLY
// Put the html widget in the same place as the CogTool widget
// (adjusting for the position of the html table)
//WE ARE ADDING TEN HERE TO COMPENSATE FOR LOCATION WITHIN BROWSER
//WE DO NOT WANT TO ADD THAT SINCE WE ARE SWITCHING TO AREA TAG AND DEALING WITHIN IMAGE ONLY
//POSITION AS OF NOW IS RELATIVE TO IMAGE :)
String properties = "onfocus=\"onFocus('" + name + "')\" onblur=\"onBlur()\"" +
" id=\"" + name + "\" style=\"position: absolute; left: " +
(bounds.x + 10) + "; top: " + (bounds.y + 33) +
"; width: " + bounds.width + "; height: " +
bounds.height + ";\"";
WidgetType type = widget.getWidgetType();
// don't pop up the "does not help..." message if there is no left click
// transition defined from these widgets
int ignoreButton = 0;
boolean ignoreLeftClick = (WidgetType.Check.equals(type) ||
WidgetType.TextBox.equals(type) ||
WidgetType.Graffiti.equals(type) ||
WidgetType.Menu.equals(type));
if (ignoreLeftClick) {
ignoreButton = LEFT_MOUSE;
}
else if (WidgetType.ContextMenu.equals(type)) {
ignoreButton = RIGHT_MOUSE;
}
String eventString = getEventString(widget, ignoreButton);
if (!widget.isRendered()
&& "".equals(widget.getTitle())
&& widget.getImage() == null
&& frame.getBackgroundImage() == null
&& ! WidgetType.Noninteractive.equals(type))
{
blindHotSpotWarning += "This CogTool model has a hidden widget on screen. " +
"You will not be able to visibly identify the location of +" +
widget.getName() + ".\n";
}
//Why did they not use a switch statement here ?
if (isStandard) {
if (WidgetType.Noninteractive.equals(type) ||
WidgetType.Text.equals(type))
{
return getHotspotString(widget, properties, eventString);
}
if (WidgetType.Button.equals(type)) {
if (!widget.isRendered()){
if (! "".equals(widget.getTitle())) {
html.append("<div align=\"middle\"" + properties + eventString + ">" + widget.getTitle() + "</div>\n");
}
//In the below 6 lines I create an area tag that will be placed within the map tags, within the html code,
//in order to create HotSpots over the image of the frame
mapHTML += "<area shape=\"rect\" name=value ";
mapHTML += "value ='" + widget.getTitle() + "'";
mapHTML += " onfocus=\"onFocus('" + name + "')\" onblur=\"onBlur()\"" +
" id=\"" + name + "\"" + " coords=\"" + bounds.x + "," + bounds.y + ","
+ (bounds.x+bounds.width) + "," + (bounds.y+bounds.height) + "\"";
mapHTML += eventString + "/>\n";
}
else {
html.append("<input type=button name=value value='");
html.append(widget.getTitle());
html.append("' " + properties + eventString + ">\n");
}
}
else if (WidgetType.Check.equals(type)) {
String checked = "";
Object isSel =
widget.getAttribute(WidgetAttributes.IS_SELECTED_ATTR);
if (NullSafe.equals(WidgetAttributes.IS_SELECTED, isSel)) {
checked = " checked";
}
// for checkboxes, width and height don't matter, so only deal with
// the x and y
String styleString = properties.substring(0, properties.indexOf("width")) +
"\"";
html.append("<input type=checkbox " + styleString + eventString);
html.append(checked + ">\n");
String textStyle = "style=\"position: absolute; left: " +
(bounds.x + 30) + "; top: " +
(bounds.y + 33) + ";\"";
String textEvent = eventString;
if (textSelect(widget)) {
textEvent += " onclick=\"toggleCheckbox('";
textEvent += widget.getName();
textEvent += "')\"";
}
html.append("<a " + textStyle + textEvent + ">");
html.append(widget.getTitle() + "</a>\n");
}
else if (WidgetType.Radio.equals(type)) {
SimpleWidgetGroup group = widget.getParentGroup();
if ((group != null) && ! visitedGroups.contains(group)) {
visitedGroups.add(group);
String groupName = widget.getName();
Iterator<IWidget> widgets = group.iterator();
while (widgets.hasNext()) {
IWidget w = widgets.next();
DoublePoint origin = w.getShape().getOrigin();
String itemString = getEventString(w, LEFT_MOUSE);
String checked = "";
Object isSel =
w.getAttribute(WidgetAttributes.IS_SELECTED_ATTR);
if (NullSafe.equals(WidgetAttributes.IS_SELECTED, isSel)) {
checked = " checked";
}
// for radio buttons, width and height don't matter, so only deal with
// the x and y
String styleString = "onfocus=\"onFocus('" + w.getName() +
"')\" onblur=\"onBlur()\"" +
"style=\"position: absolute; left: " +
(origin.x + 10) + "; top: " +
(origin.y + 33) + ";\"";
// TODO: if transitioned from, never deselects
html.append("<input type=radio name=\"" + groupName + "\"");
html.append(" id=\"");
html.append(w.getName());
html.append("\" " + styleString + itemString + checked + ">\n");
String textStyle = "style=\"position: absolute; left: " +
(origin.x + 30) + "; top: " +
(origin.y + 33) + ";\"";
String textEvent = itemString;
if (textSelect(w)) {
textEvent += " onclick=\"selectRadio('";
textEvent += w.getName();
textEvent += "')\"";
}
html.append("<a " + textStyle + textEvent + ">");
html.append(w.getTitle() + "</a>\n");
}
}
}
else if (WidgetType.TextBox.equals(type) ||
WidgetType.Graffiti.equals(type))
{
html.append("<input type=text name=value value='");
html.append(widget.getTitle());
html.append("' " + properties + eventString + ">\n");
}
else if (widget instanceof ListItem) {
SimpleWidgetGroup group = widget.getParentGroup();
// TODO: once list box functionality is added to CogTool, some of
// this code will change
if (! (visitedGroups.contains(group))) {
visitedGroups.add(group);
ListItem firstItem = (ListItem) group.get(0);
DoubleRectangle itemBounds = firstItem.getEltBounds();
// use properties of the first item in the list
properties = "onfocus=\"onFocus('" + firstItem.getName() +
"')\" onblur=\"onBlur()\"" + " id=\"" +
firstItem.getName() + "\" style=\"position: absolute; left: " +
(itemBounds.x + 10) + "; top: " + (itemBounds.y + 33) +
"; width: " + itemBounds.width + ";\"";
html.append("<select size=" + group.size() + " " +
properties + ">\n");
Iterator<IWidget> widgets = group.iterator();
while (widgets.hasNext()) {
IWidget w = widgets.next();
String itemEvent = getEventString(w, 0);
html.append("<option" + itemEvent + " id=\"");
html.append(w.getName() + "\">");
html.append(w.getTitle() + "\n");
}
html.append("</select>\n");
}
}
else if (widget instanceof MenuHeader) {
// make hotspot associated w/ menu, use widget's
// name for ID
// html.append("<input type=button name=value value='");
// html.append(widget.getTitle());
// html.append("' style=" + style + " id=\"");
// html.append(widget.getName());
// html.append("\">\n");
// TODO: add background color?
properties = properties.substring(0, properties.length() - 1);
properties += " background: lightGray;\"";
html.append("<div " + properties + eventString + ">");
html.append(widget.getTitle());
html.append("</div>\n");
}
else if (widget instanceof ContextMenu) {
// TODO: add background color?
// style = style.substring(0, style.length() - 1);
// style += " background: lightGray;\"";
html.append("<div " + properties + eventString + ">");
html.append(widget.getTitle());
html.append("</div>\n");
}
else if (widget instanceof PullDownHeader) {
PullDownHeader pdh = (PullDownHeader) widget;
//"select tag is used to create a select list" --> http://www.w3schools.com/TAGS/tag_Select.asp
html.append("<select " + properties + ">\n");
if (pdh.itemCount() > 0) {
//html.append("<optgroup>\n"); //TODO: style
Iterator<IWidget> children =
pdh.getChildren().iterator();
while (children.hasNext()) {
IWidget pdi = children.next();
String childEvent = getEventString(pdi, 0);
html.append("<option" + childEvent + " id=\"");
html.append(pdi.getName() + "\">" + pdi.getTitle() + "\n");
}
//html.append("</optgroup>\n");
}
html.append("</select>\n");
}
else if (WidgetType.Link.equals(type)) {
properties = properties.substring(0, properties.length() - 1);
html.append("<a " + properties + " color: blue;\"");
html.append(eventString + "><u>");
html.append(widget.getTitle());
html.append("</u></a>\n");
}
}
else {
// unknown widget or custom version of an interactive widget
return getHotspotString(widget, properties, eventString);
}
return html.toString();
}
/**
* Code for generating a generic html hotspot with bounds, transitions, and
* potentially a background image and border.
*/
protected String getHotspotString(IWidget widget,
String properties,
String eventString)
{
properties = properties.substring(0, properties.length() - 1);
ImageLoader imageLoader = new ImageLoader();
Image img = buildWidgetImage(widget);
if (img != null) {
imageLoader.data =
new ImageData[] { img.getImageData() };
String imageName = getWidgetFileName(widget, ".jpg");
// create new FILE handler for the new imageSaver's use
File imageFile = new File(parentDir, imageName);
try {
imageLoader.save(imageFile.getCanonicalPath(), SWT.IMAGE_JPEG);
}
catch (IOException ex) {
// We can continue even with exceptions on individual images
throw new ImageException("Failed saving image for HTML export",
ex);
}
properties += " background-image: url(" + imageName + ");";
// dispose the image, it's not needed any more.
img.dispose();
}
return "<div " + properties + "\"" + eventString + ">" + widget.getTitle() + "</div>\n";
}
/**
* Clean any known restricted characters from the name and return it
*
* Known restricted characters.
* Mac: ":/"
* Windows: "\"
*
* I know there are more, add when known.
* @param name
* @return
*/
protected static String cleanStringForFS(String name)
{
// name = name.replaceAll(":", "_");
// // Need to escape the \ in the compiled regex
// name = name.replaceAll("\\\\", "_");
// name = name.replaceAll("/", "_");
// name = name.replaceAll(" ", "_");
// name = name.replaceAll("'", "_"); // Causes havoc in the JS
// name = name.replaceAll("\"", "_"); // causes havoc in the JS
try {
name = URLEncoder.encode(name, "UTF-8");
// TODO here's what was there before: not clear how it ever
// compiled; maybe someone was compiling against JDK 1.5
// instead of 1.4?
// name = name.replace("+", "%20");
// here's the replacement:
name = name.replaceAll("\\+", "%20");
}
catch (UnsupportedEncodingException e) {
throw new ExportException("Could not encode frame html", e);
}
return name;
}
/**
* Build an index page from the design. This is done by creating a table
* that has 2 columns of thumbnails. Each thumbnail starts with the frame's
* title then has the frame's image. Device's are ignored.
*/
protected String buildIndexPage()
{
// Iterate over all frames alphabetically
Iterator<Frame> iter =
NamedObjectUtil.getSortedList(design.getFrames()).iterator();
// HTML buffer
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
html.append("<html>\n");
html.append("<head>\n");
html.append("<title>");
html.append(design.getName());
html.append("</title>\n");
html.append("</head>\n");
html.append("<body>\n");
// Add a set of instructions.
html.append("<h3>");
html.append("<div align=center>");
html.append(L10N.get("WB.InstructionsHeader", "Instructions:"));
html.append(L10N.get("WB.Instructions",
"<br>Select a start frame from the list of options " +
"and you will see the interface you created.<br>" +
"To interact with the interface, type on the" +
" keyboard or left-click, right-click or hover on" +
" the widgets. If the action is defined, it will" +
" show you the results. If the action is not" +
" defined, a generic message will appear saying" +
" that this action does not help accomplish the goal."));
html.append("</h3>");
html.append("</div>");
html.append("<table border=1>\n");
// use the counter to determine when to put in a new row on the table
int i = 0;
while (iter.hasNext()) {
Frame frame = iter.next();
// if ((i % 3) == 0) {
// // if not the first line, close the last row
if (i != 0) {
html.append("</tr>\n");
}
html.append("<tr>\n");
// }
html.append("<td>\n");
// Build the table cell for each frame.
html.append("<a href='" + getFrameURL(frame, ".html") + "'>");
html.append("<div>");
html.append(frame.getName());
html.append("</div><div align=right>");
// TODO do something smarter for the images.
// Ideally I would want to crop, but HTML does not have a crop that
// I know of, so I will need to get the size, and resize it
// DoubleRectangle rect = frame.getBackgroundBounds();
// double scale = 1.0;
// if (rect != null) {
// if (rect.width > rect.height) {
// // get x scale factor
// scale = 100.0 / rect.width;
// }
// else {
// scale = 100.0 / rect.height;
// }
//
// html.append("<img src='" + getFrameImageURL(frame) + "' width=");
// html.append(rect.width * scale);
// html.append(" height=");
// html.append(rect.height * scale);
// html.append("/>");
// }
// else {
// html.append("<img src='" + getFrameImageURL(frame) + "' />");
// }
html.append("</a>");
html.append("</div>");
html.append("</td>");
i++;
}
html.append("</tr>\n");
html.append("</table>\n");
html.append("</body>\n");
html.append("</html>\n");
return html.toString();
}
public String warningStrings(Design d)
{
StringBuilder result = new StringBuilder();
for (Frame frame : d.getFrames()) {
for (IWidget widget : frame.getWidgets()) {
WidgetType type = widget.getWidgetType();
if (widget.isStandard()) {
if (WidgetType.Button.equals(type)) {
if (! widget.isRendered()){
String title = widget.getTitle();
if (title == null || title.length() == 0) {
result.append("Frame \"");
result.append(frame.getName());
result.append("\" contains a hidden hot-spot button at widget \"");
result.append(widget.getName());
result.append("\".\n");
}
}
}
}
}
}
return (result.length() > 0 ? result.toString() : null);
}
}