/******************************************************************************* * 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.model; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import edu.cmu.cs.hcii.cogtool.CogTool; import edu.cmu.cs.hcii.cogtool.CogToolPref; import edu.cmu.cs.hcii.cogtool.util.Cancelable; import edu.cmu.cs.hcii.cogtool.util.FileUtil; import edu.cmu.cs.hcii.cogtool.util.IAttributed; import edu.cmu.cs.hcii.cogtool.util.KeyDisplayUtil; import edu.cmu.cs.hcii.cogtool.util.KeyboardUtil; import edu.cmu.cs.hcii.cogtool.util.L10N; import edu.cmu.cs.hcii.cogtool.util.LispUtil; import edu.cmu.cs.hcii.cogtool.util.NamedObject; import edu.cmu.cs.hcii.cogtool.util.ObjectLoader; import edu.cmu.cs.hcii.cogtool.util.ObjectSaver; import edu.cmu.cs.hcii.cogtool.util.ProcessTraceCallback; import edu.cmu.cs.hcii.cogtool.util.StringUtil; import edu.cmu.cs.hcii.cogtool.util.Subprocess; // TODO the model file we write, while capable of being run standalone, still // contains lots of detritus from our old structure; it should be tidied up // to look like decent ACT-R code someone might write by hand, or at least as // close to that as we can conveniently manage. public class ACTRPredictionAlgo extends APredictionAlgo { // The following should be incremented every time changes are made that // make the trace sufficiently different that visualization will fail. public static final int TRACE_VERSION = 2; public static final String USES_OBSOLETE_WAITS = "ACTRPredictionAlgo.usesObsoleteWaits"; public static final String TRACE_VERSION_ATTR = "ACTRPredictionAlgo.traceVersion"; private static final double ARTICULATION_TIME_PER_CHAR = 0.050; // seconds private static final double BACKGROUND_EFFORT = 0.005; // seconds // whether to emit a "keep alive" production--it's not clear whether // or not we're going to want this for real, but even if not it's is // convenient to be able to turn it on easily for debugging private static final boolean EMIT_KEEP_ALIVE = false; private static final String BACK_BUTTON_SEPARATOR = "[\n\r]+"; private static final String CTE_LOG_FILE = "cogtool-explorer.log"; // We preserve the old value here as it appears it may be needed // for reading really old .cgt files. Bit of a mystery, really. public static final int OLD_TIMEOUT_VALUE = 600; private static final String ACTR_BOILERPLATE = "edu/cmu/cs/hcii/cogtool/resources/cogtool-actr.lisp"; private static final String CT_EXPLORER_SUPPORT = "edu/cmu/cs/hcii/cogtool/resources/ct-explorer-support.lisp"; private static final String CT_EXPLORER_MODEL = "edu/cmu/cs/hcii/cogtool/resources/ct-explorer-model.lisp"; private static final String EMMA = "edu/cmu/cs/hcii/cogtool/resources/emma.lisp"; private static final double TIME_EQUALITY_TOLERANCE = 0.0001; public static boolean emitVirtualFrames = false; private static boolean equalTimes(Double t1, Double t2) { return Math.abs(t1 - t2) <= TIME_EQUALITY_TOLERANCE; } private static double getActrTimeoutInSeconds() { return CogToolPref.ACTR_TIMEOUT.getInt() / 1000.0; } public static final int edu_cmu_cs_hcii_cogtool_model_ACTRPredictionAlgo_version = 0; private static ObjectSaver.IDataSaver<ACTRPredictionAlgo> SAVER = new ObjectSaver.ADataSaver<ACTRPredictionAlgo>() { @Override public int getVersion() { return edu_cmu_cs_hcii_cogtool_model_ACTRPredictionAlgo_version; } @Override public void saveData(ACTRPredictionAlgo v, ObjectSaver saver) throws java.io.IOException { } }; public static void registerSaver() { ObjectSaver.registerSaver(ACTRPredictionAlgo.class.getName(), SAVER); } private static ObjectLoader.IObjectLoader<ACTRPredictionAlgo> LOADER = new ObjectLoader.AObjectLoader<ACTRPredictionAlgo>(); // This object will always be ignored when used during loading // of scripts and results (see TaskApplication version 0 loading) private static final ACTRPredictionAlgo StandaloneAlgo_ONLY = new ACTRPredictionAlgo(); private static ObjectLoader.IObjectLoader<ACTRPredictionAlgo> StandaloneAlgo_LOADER = new ObjectLoader.AObjectLoader<ACTRPredictionAlgo>() { @Override public ACTRPredictionAlgo createObject() { return StandaloneAlgo_ONLY; } }; // The last known version for StandaloneAlgo private static final int edu_cmu_cs_hcii_cogtool_model_StandaloneAlgo_version = 0; public static void registerLoader() { ObjectLoader.registerLoader(ACTRPredictionAlgo.class.getName(), edu_cmu_cs_hcii_cogtool_model_ACTRPredictionAlgo_version, LOADER); ObjectLoader.registerLoader("edu.cmu.cs.hcii.cogtool.model.StandaloneAlgo", edu_cmu_cs_hcii_cogtool_model_StandaloneAlgo_version, StandaloneAlgo_LOADER); } protected ACTRPredictionAlgo() { } static { IAttributed.AttributeRegistry.ONLY.defineAttribute(USES_OBSOLETE_WAITS, Boolean.class, Boolean.FALSE); IAttributed.AttributeRegistry.ONLY.defineAttribute(TRACE_VERSION_ATTR, Integer.class, new Integer(0)); } // Use of this static method should guarantee that the attribute has // been defined, since the class must be loaded to call this. public static boolean usesObsoleteWaits(APredictionResult r) { return ((Boolean) r.getAttribute(USES_OBSOLETE_WAITS)).booleanValue(); } // Use of this static method should guarantee that the attribute has // been defined, since the class must be loaded to call this. public static int getTraceVersion(APredictionResult r) { return ((Integer) r.getAttribute(TRACE_VERSION_ATTR)).intValue(); } protected class ACTRAnalysisOutput implements IAnalysisOutput { protected Script script; protected List<String> traceLines; protected List<String> errorLines; protected boolean usesObsoleteWaits; public ACTRAnalysisOutput(Script s, List<String> traces, List<String> errors, boolean usesObsWaits) { script = s; traceLines = traces; errorLines = errors; usesObsoleteWaits = usesObsWaits; } public APredictionResult completeWork() { int lineCount = traceLines.size(); TimePredictionResult result = null; if (lineCount > 0) { try { // Parse trace and return result with steps String lastLine = traceLines.get(lineCount - 1); double taskTime = Double.parseDouble(lastLine); TraceParser<ResultStep> parser = getTraceParser(); List<ResultStep> resultSteps = parser.parseTrace(traceLines); // resultSteps = TraceReducer.simplifyTrace(resultSteps); if (! equalTimes(taskTime, (double)OLD_TIMEOUT_VALUE)) { result = new TimePredictionResult("ACT-R Time: " + taskTime, script, ACTRPredictionAlgo.this, traceLines, errorLines, resultSteps, taskTime); } else { result = new TimePredictionResult("ACT-R Model Timeout", script, ACTRPredictionAlgo.this, traceLines, errorLines, resultSteps); } } catch (NumberFormatException e) { result = new TimePredictionResult("ACT-R Parse Error", script, ACTRPredictionAlgo.this, traceLines, errorLines); } } else { result = new TimePredictionResult("ACT-R Error", script, ACTRPredictionAlgo.this, traceLines, errorLines); } result.setAttribute(TRACE_VERSION_ATTR, new Integer(TRACE_VERSION)); if (usesObsoleteWaits) { result.setAttribute(USES_OBSOLETE_WAITS, Boolean.TRUE); } return result; } } protected class ACTRAnalysisInput extends AAnalysisInput { protected Script script; protected String lispMem; protected File file; protected String cmd; protected boolean usesObsoleteWaits; public ACTRAnalysisInput(Script s, String imageFile, String inputFile, String cmdStr, boolean usesObsWaits) { script = s; lispMem = imageFile; file = new File(inputFile); cmd = cmdStr; usesObsoleteWaits = usesObsWaits; } public IAnalysisOutput compute(ProcessTraceCallback progressCallback, Cancelable cancelable) { // Ignore callback until we can figure it out. List<String> traceLines = new ArrayList<String>(); List<String> errorLines = new ArrayList<String>(); List<File> files = new ArrayList<File>(1); files.add(file); if (!usesObsoleteWaits) { // Execute clisp, loading stored memory image and temp files try { Subprocess.execLisp(lispMem, files, cmd, traceLines, errorLines, progressCallback, cancelable); // ignore return value } catch (Subprocess.ExecuteException ex) { throw new ComputationException("Executing LISP failed", ex); } } return new ACTRAnalysisOutput(script, traceLines, errorLines, usesObsoleteWaits); } } protected static class ObsoleteWaitException extends RuntimeException { public ObsoleteWaitException() { super("Contains obsolete wait steps"); } } @Override public IAnalysisInput prepareComputation(Script script) { Demonstration demonstration = script.getDemonstration(); boolean usesObsoleteWaits = false; String path = script.getAssociatedPath(); File actrFile = null; if (path != null) { actrFile = new File(path); } else { try { // Create a temp file to hold the ACT-R model actrFile = File.createTempFile("cogtool-actr-model-", ".lisp"); actrFile.deleteOnExit(); TaskApplication ta = demonstration.getTaskApplication(); outputModel(ta.getDesign(), ta.getTask(), demonstration.getStartFrame(), script, actrFile, null); } catch (ObsoleteWaitException e) { usesObsoleteWaits = true; } catch (IOException e) { throw new ComputationException("IOException creating ACT-R model", e); } } return new ACTRAnalysisInput(script, "actr6.mem", actrFile.getAbsolutePath(), "*cogtool-result*", usesObsoleteWaits); } /** * Write a Lisp representation of this design to a PrintWriter. * @param d the design to output * @param startFrame the frame that should be initially active * @param out the output PrintWriter */ protected static void outputDesign(Design d, PrintWriter out) { out.println("(terpri)"); if (CogToolPref.isTraceEmitted()) { out.println("(princ \"Cogtool " + LispUtil.clean(CogTool.getVersion()) + "\")"); } else { out.println("(setq *suppress-trace* t)"); out.println("(defparameter *overridden-global-parameters* " + "'(:v nil :trace-detail low :use-tree t :ncnar nil :dcnn nil))"); } if (CogToolPref.RESEARCH.getBoolean()) { out.println("(setq *cogtool-debug* " + CogToolPref.ACTR_DEBUG_LEVEL.getInt() + ")"); } boolean useACTRDefaults = (!CogToolPref.RESEARCH.getBoolean() || !CogToolPref.ACTR_ALTERNATIVE_PARAMETERS.getBoolean()); out.format(Locale.US, ("(setq *overridden-global-parameters* `(" + ":visual-attention-latency %.3f :motor-initiation-time %.3f :peck-fitts-coeff %.3f :dat %.3f " + ",@*overridden-global-parameters*))\n"), (useACTRDefaults ? CogToolPref.VISUAL_ATTENTION.getIntDefault() : CogToolPref.VISUAL_ATTENTION.getInt()) / 1000.0, (useACTRDefaults ? CogToolPref.MOTOR_INITIATION.getIntDefault() : CogToolPref.MOTOR_INITIATION.getInt()) / 1000.0, (useACTRDefaults ? CogToolPref.PECK_FITTS_COEFF.getIntDefault() : CogToolPref.PECK_FITTS_COEFF.getInt()) / 1000.0, (useACTRDefaults ? CogToolPref.ACTR_DAT.getIntDefault() : CogToolPref.ACTR_DAT.getInt()) / 1000.0); out.println(); out.println(";; ==== Design ====\n"); out.println("(defvar *frame-definitions* nil)\n"); // Create the frames Iterator<Frame> framesIt = d.getFrames().iterator(); int i = 0; while (framesIt.hasNext()) { Frame frame = framesIt.next(); String fn = "cogtool-frame-fn-" + (++i); out.println("(defun " + fn + " ()"); out.println("\n (let ((frames (frames *cogtool-design*)) frame widget groups)"); outputFrame(frame, out); out.println("\n\n (setf (gethash " + LispUtil.safeString(frame.getName()) + " frames) frame)))\n"); out.println("(push #'" + fn + " *frame-definitions*)\n"); } out.println("(setq *frame-definitions* (nreverse *frame-definitions*))\n"); out.println("(defun define-design ()"); out.println(" (setq *cogtool-design* (make-instance 'cogtool-device))"); out.println(" (loop for f in *frame-definitions* do (funcall f)))\n"); out.println("(when *cogtool-random-seed*"); out.println(" (setf (getf *default-global-parameters* :seed) *cogtool-random-seed*))\n\n"); } private static Set<String> backButtonLabels = null; public void outputModel(Design design, AUndertaking task, Frame startFrame, Script script, File file, Map<String, Object> variablesToDefine) throws IOException { backButtonLabels = new HashSet<String>( Arrays.asList(CogToolPref.CTE_DEFAULT_BACK_LABEL.getString().split(BACK_BUTTON_SEPARATOR))); PrintWriter w = null; try { w = new PrintWriter( new BufferedWriter( new OutputStreamWriter( new FileOutputStream(file), "US-ASCII"))); if (CogToolPref.SYSWVO.getBoolean()) { w.println("(defparameter *system-wait-blocks-only-vision* t)\n"); } if (variablesToDefine != null) { // Note that the Objects that are values in the map should // be clean for printing into a form Lisp will read; integers // and doubles are fine, strings and symbols may need to be // LispUtil.clean'd+quoted or LispUtil.safeString'd before being // inserted into the Map. for (String k : variablesToDefine.keySet()) { if (k.equals("*cogtool-files-to-load*")) { continue; } w.print("(defparameter " + k + " "); Object val = variablesToDefine.get(k); if (val == null) { w.print("nil"); } else if (val instanceof Collection<?>) { w.print(LispUtil.makeList((Collection<?>) val)); } else { w.print(val); } w.println(")"); } w.println(); } w.println(";; For Design: " + design.getName()); if (task != null) { w.println(";; For Task: " + task.getName() + "\n"); } if (CogToolPref.USE_EMMA.getBoolean()) { w.println(";; EMMA\n"); FileUtil.copyTextResourceToWriter(EMMA, w); } FileUtil.copyTextResourceToWriter(ACTR_BOILERPLATE, w); outputDesign(design, w); if (script != null) { outputScript(script, startFrame, w); } else if (design.getDeviceTypes().contains(DeviceType.Touchscreen)) { w.println("\n(setq *use-finger-default-value* t)"); } //w.println("\n(load-pending-cogtool-files)"); if (variablesToDefine != null && variablesToDefine.get("*cogtool-files-to-load*") != null) { for (String s : (List<String>)variablesToDefine.get("*cogtool-files-to-load*")) { s = s.substring(1, s.length() - 1); FileUtil.copyTextFileToPrintWriter(new File(s), w); } } w.println("\n(setq *cogtool-result* (cogtool-run-model))"); } finally { if (w != null) { w.close(); } } } // While in normal CogTool we take special care of clicks and taps // in exactly the same location, this logic is not available to CT-E, // which causes trouble, particularly with taps. Therefore as a temporary // kludge until we figure out a better way to deal with it, when emitting // a design for CT-E ensure that no widgets are at identical positions. private static List<DoublePoint>frameElementPositions = null; public void outputModel(Design design, AUndertaking task, // may be null if unknown or irrelevant File file, String scoresPath, SNIFACTPredictionAlgo.SNIFACTParameters parms) throws IOException { Map<String, Object> lispVars = new HashMap<String, Object>(); lispVars.put("*task-description*", LispUtil.safeString(parms.taskName)); lispVars.put("*number-of-runs*", new Integer(parms.numRuns)); lispVars.put("*SNIF-ACT-k-value*", new Integer(parms.kValue)); lispVars.put("*CogTool-Explorer-start-frame*", LispUtil.safeString(parms.startFrame)); List<String> tFrames = new ArrayList<String>(); for (String s : parms.targetFrames) { tFrames.add(LispUtil.safeString(s)); } lispVars.put("*CogTool-Explorer-target-frames*", tFrames); List<String> files = new ArrayList<String>(); File f = File.createTempFile("cogtool-explorer-support-", ".lisp"); f.deleteOnExit(); FileUtil.copyResourceToFile(CT_EXPLORER_SUPPORT, f); String supportPath = f.getAbsolutePath(); f = File.createTempFile("cogtool-explorer-model-", ".lisp"); f.deleteOnExit(); FileUtil.copyResourceToFile(CT_EXPLORER_MODEL, f); String modelPath = f.getAbsolutePath(); files.add(LispUtil.safeString(supportPath)); files.add(LispUtil.safeString(scoresPath)); files.add(LispUtil.safeString(modelPath)); lispVars.put("*cogtool-files-to-load*", files); File logFile = new File(CogToolPref.LOG_DIRECTORY.getString(), CTE_LOG_FILE); lispVars.put("*log-file*", LispUtil.safeString(logFile.getCanonicalPath())); lispVars.put("*CT-E_timeout*", Double.toString(getActrTimeoutInSeconds())); if (! CogToolPref.CTE_SUPPRESS_NOISE.getBoolean()) { lispVars.put("*cogtool-random-seed*", "nil"); } int backButtonSemantics = CogToolPref.CTE_BACK_BUTTON_SEMANTICS.getInt(); if (backButtonSemantics != SNIFACTPredictionAlgo.IMPLICIT_BACK && backButtonSemantics != SNIFACTPredictionAlgo.NO_BACK) { lispVars.put("*use-back-button*", "t"); lispVars.put("*use-back-button-history-in-half-flatten-layout*", "t"); } else { lispVars.put("*use-back-button*", "nil"); if (backButtonSemantics == SNIFACTPredictionAlgo.IMPLICIT_BACK) { lispVars.put("*allow-magic-go-backs*", "t"); } else { lispVars.put("*allow-magic-go-backs*", "nil"); } } String logDir = CogToolPref.LOG_DIRECTORY.getString(); if (!logDir.endsWith("/")) { logDir += "/"; } lispVars.put("*log-file-directory*", LispUtil.safeString(logDir)); frameElementPositions = new ArrayList<DoublePoint>(); outputModel(design, task, null, null, file, lispVars); frameElementPositions = null; } private static Map<SimpleWidgetGroup, FrameElement> simpleWidgetGroupOwners = new HashMap<SimpleWidgetGroup, FrameElement>(); /** * Write a Lisp representation of this frame to a PrintWriter. * Uses the Lisp free variables frame, widget, frame-trans and * widget-trans, which should be bound in the current lexical scope. * @param out the output PrintWriter */ private static void outputFrame(Frame f, PrintWriter out) { virtualFrames.clear(); // redundant, but extra safety Set<FrameElement> grps = new HashSet<FrameElement>(); simpleWidgetGroupOwners.clear(); out.println("\n ;; ==== New Frame ====\n"); // Create the frame out.print(" (setq frame (make-instance 'cogtool-frame :name " + LispUtil.safeString(f.getName())); String st = f.getSpeakerText(); double duration = f.getListenTimeInSecs(); if (duration != Frame.NO_LISTEN_TIME) { st = articulateTextForDuration(st, duration); } if (st != null && st.length() > 0) { out.print(" :speaker-text " + LispUtil.safeString(st)); } out.print("))"); for (IWidget widget : f.getWidgets()) { if (widget.getWidgetType() == WidgetType.Noninteractive && StringUtil.isNullOrEmpty(widget.getTitle()) && StringUtil.isNullOrEmpty(widget.getAuxiliaryText()) && CogToolPref.CTE_SUPPRESS_NONINTERACTIVE.getBoolean()) { continue; } if (emitVirtualFrames) { if (widget instanceof MenuHeader || widget instanceof ContextMenu || widget instanceof PullDownHeader || (widget instanceof MenuItem && ((MenuItem)widget).isSubmenu())) { addVirtualFrame(f.getName(), (AParentWidget)widget); } else if (widget instanceof MenuItem || widget instanceof PullDownItem) { VirtualFrame vf = virtualFrames.get(((ChildWidget)widget).getParent()); if (vf != null) { vf.items.add((ChildWidget)widget); } continue; } } outputWidget(widget, grps, out); out.print(" (push widget (widgets frame))"); } out.println(); out.println(); out.print(" (setq groups '())"); Set<FrameElement> emittedGrps = new HashSet<FrameElement>(); while (! grps.isEmpty()) { Set<FrameElement> newGrps = new HashSet<FrameElement>(); for (FrameElement g : grps) { if (! emittedGrps.contains(g)) { outputFrameElement(g, null, null, f, newGrps, out); out.print(" (push widget groups)"); emittedGrps.add(g); } } grps = newGrps; } out.println(); out.println(); out.print(" (resolve-widget-refs frame groups)"); simpleWidgetGroupOwners.clear(); for (VirtualFrame vf : virtualFrames.values()) { vf.outputSelf(out); } virtualFrames.clear(); } private static class VirtualFrame { private final String name; private List<ChildWidget> items = new ArrayList<ChildWidget>(); private VirtualFrame(String nm) { name = nm; } private void outputSelf(PrintWriter out) { out.print(String.format("\n\n (let ((subframe (make-instance 'cogtool-frame :name %s)))", LispUtil.safeString(name))); for (ChildWidget it : items) { outputWidget(it, null, out); out.print(" (push widget (widgets subframe))"); } out.print("\n (setf (gethash " + LispUtil.safeString(name) + " frames) subframe)) ; end subframe"); } } private static Map<AParentWidget, VirtualFrame> virtualFrames = new HashMap<AParentWidget, VirtualFrame>(); private static void addVirtualFrame(String parentName, AParentWidget header) { virtualFrames.put(header, new VirtualFrame(String.format("Virtual Subframe %d of %s", (virtualFrames.size() + 1), parentName))); } private static String articulateTextForDuration(String text, double duration) { int chars = Math.round((float) (duration / ARTICULATION_TIME_PER_CHAR)); if (chars <= 0) { chars = 1; } if (text != null && chars <= text.length()) { return text.substring(0, chars); } StringBuilder result = new StringBuilder(chars); if (text != null) { result.append(text); } while (result.length() < chars) { result.append('X'); } return result.toString(); } // cogtool-device.lisp expects a set of string, widget type names. // Formerly, these were generated from the display names for the various // widget types, but that would break when we were localized, so these // fixed names are provided here instead. // XXXX: The case sensitive comparison of strings for types in // cogtool-device.lisp probably isn't the right thing to be doing. // Rather, we should probably be doing eq comparison on symbols, // or something like that. This needs to be thought through further // at some point. protected static final Map<WidgetType, String> widgetTypeNames = new HashMap<WidgetType, String>(12); static { widgetTypeNames.put(WidgetType.Button, "cogtool-button"); widgetTypeNames.put(WidgetType.Link, "cogtool-link"); widgetTypeNames.put(WidgetType.ContextMenu, "cogtool-context-menu"); widgetTypeNames.put(WidgetType.Menu, "cogtool-menu"); widgetTypeNames.put(WidgetType.Submenu, "cogtool-submenu"); widgetTypeNames.put(WidgetType.MenuItem, "cogtool-menu-item"); widgetTypeNames.put(WidgetType.TextBox, "cogtool-text-box"); widgetTypeNames.put(WidgetType.Text, "cogtool-text"); widgetTypeNames.put(WidgetType.PullDownList, "cogtool-pull-down-list"); widgetTypeNames.put(WidgetType.PullDownItem, "cogtool-pull-down-item"); widgetTypeNames.put(WidgetType.ListBoxItem, "cogtool-list-box-item"); widgetTypeNames.put(WidgetType.Radio, "cogtool-radio-button"); widgetTypeNames.put(WidgetType.Check, "cogtool-checkbox"); widgetTypeNames.put(WidgetType.Graffiti, "cogtool-graffiti"); widgetTypeNames.put(WidgetType.Noninteractive, "cogtool-non-interactive"); } /** * Write a Lisp representation of this widget to a PrintWriter. * Uses the Lisp free variables widget and * widget-trans, which should be bound in the current lexical scope. * @param out the output PrintWriter */ private static void outputWidget(IWidget widget, Set<FrameElement> allGrps, PrintWriter out) { outputFrameElement(widget, widget.getTitle(), widget.getWidgetType(), widget.getFrame(), allGrps, out); // Note that we never actually use the transition stuff at all, and // don't need it for CogTool per se. However Leonghwee's SNIF-ACT stuff // does need this, so we should be sure to keep this alive. // Create transitions if (widget.getTransitions().size() > 0) { out.print(" (let ((widget-trans (transitions widget)))"); for (Transition t : widget.getTransitions().values()) { AAction a = t.getAction(); if (! (a instanceof ButtonAction || a instanceof TapAction)) { // TODO For now we're just doing buttons, since that's all // Leonghwee needs; more needs to be added soon continue; } out.print("\n (setf (gethash "); outputAction(t.getAction(), out); out.print(" widget-trans)\n "); outputTransition(t, out); out.print(")"); } out.println(")"); } else if (emitVirtualFrames && (widget instanceof MenuHeader || widget instanceof ContextMenu || widget instanceof PullDownHeader || (widget instanceof MenuItem && ((MenuItem)widget).isSubmenu()))) { out.print(" (let ((widget-trans (transitions widget)))"); out.print("\n (setf (gethash '((click left)) widget-trans)\n "); outputTransition(virtualFrames.get(widget).name, out); out.println("))"); } } /** * Write a Lisp representation of this widget to a PrintWriter. * Uses the Lisp free variables widget and * widget-trans, which should be bound in the current lexical scope. * @param out the output PrintWriter */ private static void outputFrameElement(FrameElement fe, String title, WidgetType typ, Frame frame, Set<FrameElement> allGrps, PrintWriter out) { out.println(); out.println(); out.print(" (setf widget (make-instance 'cogtool-widget :name "); out.print(safeGlobalName(fe, frame)); if (title != null && title.length() > 0) { out.print(" :title "); out.print(LispUtil.safeString(title)); } out.println(); String aux = fe.getTextualCue(); if (aux != null && aux.length() > 0) { out.print(" :textual-cue "); out.println(LispUtil.safeString(aux)); } DoubleRectangle bounds = new DoubleRectangle(fe.getEltBounds()); if (frameElementPositions != null) { while (isExisitingPosition(bounds)) { bounds.x += 10; } frameElementPositions.add(new DoublePoint(bounds.x, bounds.y)); } String wTypeName = null; if (typ == null) { wTypeName = "cogtool-group"; } else { wTypeName = widgetTypeNames.get(typ); if (wTypeName == null) { throw new ComputationException("Unknown widget type: " + typ.toString()); } } out.print(" :x "); out.print(bounds.getX()); out.print(" :y "); out.print(bounds.getY()); out.print(" :width "); out.print(bounds.getWidth()); out.print(" :height "); out.println(bounds.getHeight()); if (title != null && backButtonLabels.contains(title)) { out.println(" :is-back-button t"); } out.print(" :wtype '"); out.print(wTypeName); FrameElement remoteLabelOwner = fe.getRemoteLabelOwner(); if (remoteLabelOwner != null) { IWidget remoteLabel = (IWidget) fe.getAttribute(WidgetAttributes.REMOTE_LABEL_ATTR); if (remoteLabel != null) { out.println(); out.print(" :has-remote-label "); out.print(safeGlobalName(remoteLabel)); } } Set<FrameElementGroup> grps = fe.getEltGroups(); if (grps != null && ! grps.isEmpty()) { allGrps.addAll(grps); out.println(); out.print(" :member-of-groups "); String sep = "'("; for (FrameElementGroup g : grps) { out.print(sep); sep = " "; out.print(safeGlobalName(g, frame)); } out.print(")"); } if (fe instanceof AParentWidget) { SimpleWidgetGroup swg = ((AParentWidget)fe).getChildren(); if (swg != null) { simpleWidgetGroupOwners.put(swg, fe); } } if (fe instanceof SimpleWidgetGroup) { FrameElement owner = simpleWidgetGroupOwners.get(fe); if (owner != null) { out.println(); out.print(" :member-of-groups '("); out.print(safeGlobalName(((AParentWidget)owner).getParentGroup(), frame)); out.print(")"); } } if (fe instanceof IWidget) { IWidget widget = (IWidget)fe; SimpleWidgetGroup pgrp = widget.getParentGroup(); if (pgrp != null && allGrps != null) { allGrps.add(pgrp); out.println(); out.print(" :member-of-groups '("); out.print(safeGlobalName(pgrp, frame)); out.print(")"); } } out.println("))"); } private static boolean isExisitingPosition(DoubleRectangle bnds) { for (DoublePoint p : frameElementPositions) { if (Math.abs(p.x - bnds.x) < 2.1 && Math.abs(p.y - bnds.y) < 2.1) { return true; } } return false; } public static void outputTransition(String destName, PrintWriter out) { out.print("(make-instance 'cogtool-transition :target \""); out.print(LispUtil.clean(destName)); out.print("\")"); } /** * Write a lisp representation of this transition to a PrintWriter. * @param out the output PrintWriter */ public static void outputTransition(Transition t, PrintWriter out) { outputTransition(t.getDestination().getName(), out); } /** * Write a lisp representation of the trigger for this action to a * PrintWriter. * @param out the output PrintWriter */ public static void outputAction(AAction a, final PrintWriter out) { if (! (a instanceof ButtonAction)) { // TODO this works for now when all we care about is clicks and taps // on buttons, but we'll need to do something more sophisticated // when we support inferring transitions on the Lisp side // for all widgets x out.print("'((click left))"); return; } ButtonAction ba = (ButtonAction) a; MouseButtonState btn = ba.getButton(); MousePressType bpt = ba.getPressType(); // Clicks are represented by lists of symbol-tuples String button = null; if (btn == MouseButtonState.Left) { button = "left"; } else if (btn == MouseButtonState.Middle) { button = "middle"; } else if (btn == MouseButtonState.Right) { button = "right"; } else if (bpt != MousePressType.Hover || btn != null) { throw new IllegalStateException("Unrecognized MouseButtonState: " + btn); } // TODO: respond to mouse down/up in Lisp if (bpt == MousePressType.Click) { out.print("'((click " + button + "))"); } else if (bpt == MousePressType.Double) { out.print("'((click " + button + ") (click" + button + "))"); } else if (bpt == MousePressType.Triple) { out.print("'((click " + button + ") (click" + button + ") (click" + button + "))"); } else if (bpt == MousePressType.Down) { out.print("'((down-click " + button + "))"); } else if (bpt == MousePressType.Up) { out.print("'((up-click " + button + "))"); } else if (bpt == MousePressType.Hover) { out.print("'((hover))"); } else { throw new IllegalStateException("Unrecognized MousePressType: " + bpt); } } /** * Encodes a character into the textual form that Lisp will read as what * ACT-R expects as the name of a key. This is some sort of atom. For a * digit it is the fixnum, and otherwise a symbol. For individual letters * it is a symbol whose name is just that letter, and otherwise a word * (such as "return") describing the key. ACT-R actually uses upper-case * letters to describe keys, even though they are unshifted; we currently * map both upper and lower case letters to the same key. * * @param val the character to encode as a key * @return the textual representation of the corresponding Lisp object that * ACT-R uses to describe the key */ protected static String encodeKey(char val, boolean strict) { // Unfortunately, Character.isDigit says 'true' for too many values! if (('0' <= val) && (val <= '9')) { return Character.toString(val); } // Unfortunately, Character.isLetter says 'true' for too many values! if (('a' <= val) && (val <= 'z')) { return Character.toString(Character.toUpperCase(val)); } if (('A' <= val) && (val <= 'Z')) { return Character.toString(val); } switch (val) { // TODO: below several newly supported characters are mapped to // other characters for testing; when I've put the correct // support into ACT-R, these should be reverted to the // real things case ' ': return "space"; case ',': return "comma"; case '.': return "period"; case '/': return "slash"; case ';': return "semicolon"; case '\'': return "quote"; case '`': return "backquote"; case '-': return "hyphen"; case '=': return "hyphen"; // "equals"; case '\t': case KeyboardUtil.TAB_CHAR: return "tab"; case '[': return "P"; // "left-bracket"; case ']': return "P"; // "right-bracket"; case '\\': return "backslash"; case '\r': case '\n': case KeyboardUtil.CR_CHAR: return "return"; case KeyboardUtil.CAPSLOCK_CHAR: return "Z"; // "caps-lock"; // TODO: perhaps backspace and delete should be different "keys"? case '\b': case '\177': case KeyboardUtil.BS_CHAR: case KeyboardUtil.DEL_CHAR: return "delete"; case KeyboardUtil.MENU_CHAR: // return "menu-key"; return "."; case KeyboardUtil.ESC_CHAR: return "Q"; // "escape"; case KeyboardUtil.SHIFT_CHAR: // return "left-shift"; case KeyboardUtil.LEFT_SHIFT: // return "left-shift"; case KeyboardUtil.CTRL_CHAR: // return "left-ctrl"; case KeyboardUtil.LEFT_CTRL: // return "left-ctrl"; case KeyboardUtil.ALT_CHAR: // return "left-alt"; case KeyboardUtil.LEFT_ALT: // return "left-alt"; case KeyboardUtil.COMMAND_CHAR: // return "left-cmd"; case KeyboardUtil.LEFT_COMMAND: // return "left-cmd"; case KeyboardUtil.FUNCTION_CHAR: // return "left-fn"; case KeyboardUtil.LEFT_FUNCTION: // return "left-fn"; return "Z"; case KeyboardUtil.RIGHT_SHIFT: // return "right-shift"; case KeyboardUtil.RIGHT_CTRL: // return "right-ctrl"; case KeyboardUtil.RIGHT_ALT: // return "right-alt"; case KeyboardUtil.RIGHT_COMMAND: // return "right-cmd"; case KeyboardUtil.RIGHT_FUNCTION: // return "right-fn"; return "/"; case KeyboardUtil.UP_ARROW_CHAR: // return "up-arrow"; case KeyboardUtil.DOWN_ARROW_CHAR: // return "down-arrow"; case KeyboardUtil.LEFT_ARROW_CHAR: // return "left-arrow"; case KeyboardUtil.RIGHT_ARROW_CHAR: // return "right-arrow"; return "slash"; default: if (strict) { throw new ComputationException("Unsupported character: " + val + " (" + ((int) val) + ")"); } else { return "X"; } } } protected static final String[] ACTR_FINGERS = new String[] { // finger constants are one-based, so they're just like // piano fingering null, "thumb", "index", "middle", "ring", "pinkie" }; protected static final double STD_LOOKAT_DURATION = 0.15; protected static abstract class ScriptStepWriter extends AScriptStep.ScriptStepVisitor { protected ThinkScriptStep prevMental = null; protected int state = 1; protected Script script; protected PrintWriter out; protected int nextStep = 1; protected int stepCount; protected int nameCounter = 1; // A move-mouse followed immediately by a tap should always be // treated as a move-and-tap! protected boolean actAsMoveAndTap = false; protected IWidget lastLookedAtWidget = null; protected IWidget lastMovedToWidget = null; protected StringBuilder properties = new StringBuilder(); public ScriptStepWriter(Script s, PrintWriter w) { script = s; out = w; stepCount = script.getStepStates().size(); } abstract public void startScript(Frame startFrame); public void outputScriptStepState(DefaultModelGeneratorState stepState) { AScriptStep step = stepState.getScriptStep(); TransitionSource lookAtTarget = step.getStepFocus(); // We track the last looked-at widget internally since // the model generation may not generate look-at's // that ACT-R requires. if ((lookAtTarget instanceof IWidget) && ! ((IWidget) lookAtTarget).sameLocation(lastLookedAtWidget) && ! (step instanceof LookAtScriptStep)) { // If there is a pending mental step, decrease its duration and // generate its associated production. //TODO:mlh trying to make it look like it did in the past // decrAndFlushMentalStep(STD_LOOKAT_DURATION); flushMentalStep(); // Now insert the LookAt, but only if it's not typing if (! (step instanceof TextActionSegment)) { generateLookAt((IWidget) lookAtTarget); } } else { // If a mental is pending, generate its associated production. flushMentalStep(); } // Generate the production(s) for this script step step.accept(this); lastMovedToWidget = stepState.getLastMovedToWidget(); resetForNextStep(); } protected void generateLookAt(IWidget targetWidget, Frame destinationFrame) { // Overriding methods are expected to invoke super.generateLookAt // as their last statement. lastLookedAtWidget = targetWidget; } protected void generateLookAt(IWidget targetWidget) { // A null destinationFrame means no transition! generateLookAt(targetWidget, null); } protected void registerMentalStep(ThinkScriptStep mentalStep) { // Although two mentals should never occur in sequence, // handle anyway. if (prevMental != null) { flushMentalStep(); } prevMental = mentalStep; } protected void decrAndFlushMentalStep(double decrBy) { if (prevMental != null) { generateMental(prevMental, prevMental.getThinkDuration() - decrBy); } } protected abstract void generateMental(ThinkScriptStep step, double thinkDuration); public void flushMentalStep() { if (prevMental != null) { generateMental(prevMental, prevMental.getThinkDuration()); prevMental = null; } } protected void resetForNextStep() { // If subclass overrides, should invoke super.resetForNextStep last nextStep++; } protected boolean cursorAtFocus(TransitionSource focus) { return (focus instanceof IWidget) && ((IWidget) focus).sameLocation(lastMovedToWidget); } } public void outputScript(Script script, Frame startFrame, PrintWriter out) { out.println(";; ==== Script ==== \n"); ACTR6ScriptStepWriter ssWriter = (ACTR6ScriptStepWriter) startScriptOutput(script, startFrame, out); Iterator<DefaultModelGeneratorState> stepStateIt = script.getStepStates().iterator(); while (stepStateIt.hasNext()) { DefaultModelGeneratorState stepState = stepStateIt.next(); ssWriter.outputScriptStepState(stepState); } ssWriter.flushMentalStep(); ssWriter.endScript(); endScriptOutput(script, out); } private static WeakHashMap<Frame, ArrayList<NamedObject>> implicitGroups = new WeakHashMap<Frame, ArrayList<NamedObject>>(); public static int putImplicitGroup(NamedObject thing, Frame frame) { ArrayList<NamedObject> grps = implicitGroups.get(frame); if (grps == null) { grps = new ArrayList<NamedObject>(); implicitGroups.put(frame, grps); } int result = grps.indexOf(thing); if (result < 0) { result = grps.size(); grps.add(thing); } return result + 1; } public static NamedObject getImplicitGroup(int i, Frame frame) { --i; ArrayList<NamedObject> grps = implicitGroups.get(frame); if (grps == null || i >= grps.size()) { return null; } return grps.get(i); } // For seriously localizing this it shouldn't just be an infix, since // that might not be an appropriate scheme for non-English languages. // However, this really isn't intended for presentation to the user, // and even localizing this much is probably silly.... private static final String GLOBAL_NAME_INFIX = L10N.get("lisp.infix", " in "); protected static String safeGlobalName(NamedObject thing, Frame frame) { String name = (thing != null ? thing.getName() : null); if (name == null) { name = String.format("GROUP [i%d]", putImplicitGroup(thing, frame)); } return LispUtil.safeString(name + GLOBAL_NAME_INFIX + frame.getName()); } /** * Returns a name for a widget that is unique across the whole design, * suitably escaped for inclusion in Lisp code. */ protected static String safeGlobalName(TransitionSource widget) { return safeGlobalName(widget, widget.getFrame()); } protected String getActRVersion() { return "6"; } protected TraceParser<ResultStep> getTraceParser() { return new ACTRTraceParser(); } protected ScriptStepWriter startScriptOutput(Script script, Frame startFrame, PrintWriter out) { Demonstration demonstration = script.getDemonstration(); DefaultModelGeneratorState initialState = demonstration.getInitialState(); boolean mouseHand = demonstration.getMouseHand(); HandLocation handStartLoc = initialState.getHandLocation(mouseHand); out.println("(define-cogtool-model (:start-with-mouse " + ((handStartLoc == HandLocation.OnMouse) ? "t" : "nil") + " :timeout " + getActrTimeoutInSeconds() + ")\n"); ScriptStepWriter result = new ACTR6ScriptStepWriter(script, out, mouseHand); result.startScript(startFrame); return result; } protected void endScriptOutput(Script script, PrintWriter out) { out.println(")"); } protected static class ACTR6ScriptStepWriter extends ScriptStepWriter { protected boolean mouseHand; public ACTR6ScriptStepWriter(Script s, PrintWriter outWriter, boolean mHand) { super(s, outWriter); mouseHand = mHand; } protected static final Pattern NEEDS_ESCAPE = Pattern.compile("[\\\\$]"); protected static String escapeForRegex(String s) { return NEEDS_ESCAPE.matcher(s).replaceAll("\\\\$0"); } protected static final Pattern TEMPLATE_ITEM = Pattern.compile("%(\\d*)([a-zA-Z%])"); protected boolean handToHomePending = false; protected StringBuffer productionStr = new StringBuffer(); // ACT-R productions are written using a simple template language, // rather like a much reduced printf in C. The template is emitted // verbatim, except items preceded by % are substituted. The %items // may have an optional, non-negative numeric argument between the % // and the letter naming the kind of substitution; if not supplied this // argument defaults to zero. For example, %a and %0a are exactly // equivalent. Substitutions are // %c is the number of the production, used to ensure unique names; // the numeric argument is ignored; // %s is the current state; if a non-zero numeric argument is // supplied the value substituted is the current state plus that // argument. // %S is like %s, but also sets the stored state to that new value; // note that %0S (or equivalently %S) has no additional meaning // beyond %s, and is therefore disallowed since any attempt to use // it probably reflects some misunderstanding! // %f is the current frame name // %a substitutes additional values, supplied as additional arguments; // these are numbered starting with 0: // the first argument is %0a or just %a; the next %1a, and so on // %p substitutes a Lisp s-expression that sets the cogtool-plist; // the numeric argument is ignored protected void emitProduction(AScriptStep step, boolean checkTransition, String template, String... substitutions) { Frame destination = null; TransitionDelay delay = null; if (checkTransition) { destination = step.getDestinationFrame(); if (step instanceof ActionScriptStep) { ActionScriptStep actionStep = (ActionScriptStep) step; double delayInSecs = actionStep.getDelayInSecs(); if (delayInSecs > 0.0) { delay = actionStep; } } else if (step.isInsertedByUser()) { AScriptStep owner = step.getOwner(); if (owner instanceof TransitionScriptStep) { delay = ((TransitionScriptStep) owner).getTransition(); } else if (owner instanceof ActionScriptStep) { delay = (ActionScriptStep) owner; } } } emitProduction(step.getCurrentFrame(), destination, delay, template, substitutions); } protected void emitProduction(Frame currentFrame, Frame destinationFrame, TransitionDelay delay, String template, String... substitutions) { handToHomePending = false; actAsMoveAndTap = false; if (currentFrame != null) { addProperty(":current-frame", LispUtil.safeString(currentFrame.getName())); } if ((destinationFrame != null)) { boolean emitDestinationFrame = false; // Check to see if this step triggered a transition; // if the next step's destination is not null, and it's // a different frame, then it is a (non-self) transition. if (destinationFrame != currentFrame) { emitDestinationFrame = true; } if (delay != null) { double duration = delay.getDelayInSecs(); if (duration > 0.0) { addProperty(":delay-duration", Double.toString(duration)); addProperty(":delay-label", LispUtil.safeString(delay.getDelayLabel())); // need to "follow" the transition even if a // self-transition if there's a system wait attached to it emitDestinationFrame = true; } } if (emitDestinationFrame) { addProperty(":destination-frame", LispUtil.safeString(destinationFrame.getName())); } } Matcher m = TEMPLATE_ITEM.matcher(template); while (m.find()) { String rep = null; String arg = m.group(1); int n = 0; if (arg.length() > 0) { n = Integer.parseInt(arg); } switch (m.group(2).charAt(0)) { // Don't use %n for anything. Because the templates are so // full of \n's, using %n makes it too hard to read. case 'c': rep = Integer.toString(nameCounter); break; case 's': rep = Integer.toString(state + n); break; case 'S': if (n == 0) { throw new IllegalArgumentException( "%0S (or %S) is unlikely to be what you " + "really want; use %s instead"); } state += n; rep = Integer.toString(state); break; case 'f': if (currentFrame == null) { throw new IllegalStateException("No current frame"); } rep = escapeForRegex(LispUtil.safeString(currentFrame.getName())); break; case 'a': if (n >= substitutions.length) { throw new IllegalArgumentException( "Insufficient substitutions provided: (" + n + ", " + substitutions.length + ")"); } rep = escapeForRegex(substitutions[n]); break; case 'p': rep = escapeForRegex("(set-cogtool-plist " + properties.toString() + ")"); break; case '%': rep = "%"; break; } m.appendReplacement(productionStr, rep); } m.appendTail(productionStr); productionStr.append("\n"); out.println(productionStr.toString()); productionStr.delete(0, productionStr.length()); ++nameCounter; properties.delete(0, properties.length()); } protected void addProperty(String key, String value) { if (properties.length() > 0) { properties.append(" "); } properties.append(key); properties.append(" "); properties.append(value); } protected void handleCharacters(AScriptStep step, String text) { // Encode multiple-character key commands BitSet boundaries = new BitSet(text.length()); List<String> words = new ArrayList<String>(); findWordBoundaries(text, boundaries, words); boolean eFree = true; if (handToHomePending && text.length() > 0) { char ch = text.charAt(0); if (KeyboardUtil.needsLeftHand(ch) == (mouseHand == HandLocation.RIGHT_HAND)) { eFree = false; } } Boolean previousLeft = null; for (int i = 0; i < text.length(); i++) { if (boundaries.get(i)) { String word = LispUtil.makeSymbol(words.remove(0)); emitProduction(step, false, "(p get-spelling-%a-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?frame>\n" + " name %f\n" + " ?manual>\n" + " preparation free\n" + (eFree ? " execution free\n" : "") + "==>\n" + " !eval! %p\n" + " =goal>\n" + " state %1S)\n\n", word); eFree = true; } char ch = text.charAt(i); String key = encodeKey(ch, true); boolean usesLeft = KeyboardUtil.needsLeftHand(ch); boolean sameHand = (previousLeft == Boolean.valueOf(usesLeft)); previousLeft = Boolean.valueOf(usesLeft); String disp = Character.toString(ch); disp = KeyDisplayUtil.convertActionToMenuText(disp); addProperty(":key", LispUtil.safeString(disp)); addProperty(":hand", (usesLeft ? "'LEFT" : "'RIGHT")); emitProduction(step, ((i + 1) >= text.length()), "(p press-key-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?frame>\n" + " name %f\n" + " ?manual>\n" + " preparation free\n" + "%0a" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa press-key\n" + " key %1a\n" + " =goal>\n" + " state %1S)", (sameHand ? " execution free\n" : ""), key); } } protected static final Pattern WORD = Pattern.compile("[^ -@\\[-`\\{-~\\s]{2,}"); // TODO this will not work if the text contains non-ASCII // punctuation characters; we don't support that so it's // probably not a big deal, but should be born in mind if // we ever start paying attention to non-English -- even in // a Latin script we'll run into funny punctuation, such as // guillemonts and the like. protected void findWordBoundaries(String text, BitSet boundaries, List<String> words) { // Only words of length greater than one are considered "words". // Boundaries is used to describe where the words start. Matcher m = WORD.matcher(text); while (m.find()) { boundaries.set(m.start()); words.add(m.group(0)); } } @Override public void startScript(Frame startFrame) { if (EMIT_KEEP_ALIVE) { emitProduction(null, null, null, "(p wait-background-%c\n" + " =goal>\n" + " isa klm\n" + "==>\n" + " !eval! %p)\n\n" + "(spp wait-background-%c :u -100.0 :at %0a)", Double.toString(BACKGROUND_EFFORT)); } emitProduction(null, null, null, "(p *suppress-re-encoding-%c\n" + " =goal>\n" + " isa klm\n" + " suppress-re-encoding t\n" + " ?visual>\n" + " state free\n" + " ?frame>\n" + " status visible\n" + "==>\n" + " +visual>\n" + " isa clear\n" + " =goal>\n" + " suppress-re-encoding nil)\n" + "(spp *suppress-re-encoding-%c :at 0.0)"); emitProduction(null, startFrame, null, "(p set-start-frame-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + "==>\n" + " !eval! %p\n" + " =goal>\n" + " state %1S)\n\n" + "(spp set-start-frame-%c :at 0.0)"); } public void endScript() { emitProduction(null, null, null, "(p stop-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + "==>\n" + " !eval! %p\n" + " +goal>\n" + " isa stop)\n\n" + "(spp stop-%c :at 0.0)"); } protected void startVisit(AScriptStep step, String stepKind) { out.print(";; ScriptStep " + stepKind); if (step != null) { String s = step.getLocalizedString(); out.print(" (" + LispUtil.stripNonASCII(s) + ")"); if (step.isInsertedByUser()) { out.print(" [demonstrated]"); } } out.println("\n"); } @Override public void visit(TextActionSegment step) { handleCharacters(step, step.getText()); } @Override public void visit(HearScriptStep step) { double duration = step.getListenTimeInSecs(); String speakerText = step.getTextToHear(); String traceText = speakerText; Frame frame = step.getCurrentFrame(); if (duration == Frame.NO_LISTEN_TIME) { if (speakerText != null) { duration = articulationTime(speakerText); } } else { speakerText = articulateTextForDuration(speakerText, duration); // TODO The following may not really make sense in all languages, // as it assumes an infix preposition, and postfix units if (traceText != null && traceText.length() > 0) { traceText += (" " + L10N.get("lisp.duratonString", "for") + " "); } else { traceText = ""; } traceText += (duration + "s"); } // TODO this interacts badly if the user is speaking, as // ACT-R hears what it is saying itself, and becomes confused if (duration != Frame.NO_LISTEN_TIME) { addProperty(":spoken-text", LispUtil.safeString(speakerText)); addProperty(":trace-text", LispUtil.safeString(traceText)); emitProduction(frame, null, null, "(p queue-sound-to-hear-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?aural>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " =goal>\n" + " state %1S)\n\n" + "(spp queue-sound-to-hear-%c :at 0.0)"); emitProduction(frame, null, null, "(p listen-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " =aural-location>\n" + " isa audio-event\n" + " kind word\n" + " location loudspeaker\n" + " ?aural>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +aural>\n" + " isa sound\n" + " event =aural-location\n" + " =goal>\n" + " state %1S)"); emitProduction(frame, null, null, "(p wait-for-listening-to-finish-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?aural>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " =goal>\n" + " state %1S)\n\n" + "(spp wait-for-listening-to-finish-%c :at 0.0)"); } } @Override public void visit(ActionScriptStep step) { visitAction(step, step.getAction(), step.getStepFocus(), null); } @Override public void visit(TransitionScriptStep step) { Transition t = step.getTransition(); visitAction(step, t.getAction(), t.getSource(), t); } protected double articulationTime(String text) { // This is the same crude algorithm that ACT-R uses double result = (ARTICULATION_TIME_PER_CHAR * text.length()); return (Math.round(1000 * result) / 1000.0); // round to 3 places } protected void visitAction(AScriptStep step, AAction action, TransitionSource source, TransitionDelay delay) { ActionType type = action.getType(); if (type.equals(ActionType.ButtonPress)) { // If a multiple click, emit multiple click-mouse's, // but only transition on the last MousePressType pt = ((ButtonAction) action).getPressType(); int times = 1; if (pt.equals(MousePressType.Double)) { times = 2; } else if (pt.equals(MousePressType.Triple)) { times = 3; } String target = safeGlobalName(source); startVisit(step, "BUTTON-PRESS"); if (pt.equals(MousePressType.Down)) { addProperty(":target", target); emitProduction(step, true, "(p down-click-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa finger-down\n" + " hand right\n" + " finger index\n" + " =goal>\n" + " state %1S)"); } else if (pt.equals(MousePressType.Up)) { addProperty(":target", target); emitProduction(step, true, "(p up-click-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa finger-up\n" + " hand right\n" + " finger index\n" + " =goal>\n" + " state %1S)"); } else { for (int i = 1; i <= times; ++i) { addProperty(":target", target); emitProduction(step, false, "(p prepare-click-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " preparation free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa prepare\n" + " style punch\n" + " hand right\n" + " finger index\n" + " =goal>\n" + " state %1S)"); addProperty(":target", target); emitProduction(step, (i == times), "(p click-mouse-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " execution free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa click-mouse\n" + " =goal>\n" + " state %1S)"); } } } else if (type.equals(ActionType.MouseOver) || type.equals(ActionType.MoveMouse)) { // Peek at the next step; to fix a bug, if we see a Tap, // treat the tap as a move-and-tap instead! boolean tapIsNext = false; startVisit(step, (type.equals(ActionType.MouseOver) ? "HOVER" : "MOVE-MOUSE")); if (nextStep < stepCount) { DefaultModelGeneratorState peekStepState = script.getStepState(nextStep); AScriptStep peekStep = peekStepState.getScriptStep(); if (peekStep instanceof ActionScriptStep) { ActionScriptStep peekActionStep = (ActionScriptStep) peekStep; tapIsNext = (peekActionStep.getAction().getType() == ActionType.Tap); } } if (tapIsNext) { // Since tap is next, treat the upcoming tap as // move-and-tap and ignore this move! actAsMoveAndTap = true; } else { String target = safeGlobalName(source); // TODO: possible that we're looking for a transition on a move-mouse // even though the transition occurs later on a hover! emitProduction(step, false, "(p find-location-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?frame>\n" + " name %f\n" + " status visible\n" + "==>\n" + " !eval! %p\n" + " +visual-location>\n" + " isa visual-location\n" + " value %a\n" + " =goal>\n" + " state %s)\n\n" + "(spp find-location-%c :u -1.0)", target); emitProduction(step, true, "(p move-mouse-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " =visual-location>\n" + " isa visual-location\n" + " value %a\n" + " ?manual>\n" + " preparation free\n" + " execution free\n" + " state free\n" + " ?frame>\n" + " name %f\n" + " status visible\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa move-cursor\n" + " loc =visual-location\n" + " =visual-location>\n" + " =goal>\n" + " state %1S)", target); } } else if (type.equals(ActionType.KeyPress)) { startVisit(step, "KEY-PRESS"); handleCharacters(step, ((KeyAction) action).getText()); } else if (type.equals(ActionType.GraffitiStroke)) { String cmd = ((GraffitiAction) action).getText(); startVisit(step, "GRAFFITI"); // Encode multiple-character graffiti commands for (int i = 0; i < cmd.length(); i++) { String key = encodeKey(cmd.charAt(i), false); addProperty(":key", ("'" + key)); emitProduction(step, ((i + 1) >= cmd.length()), "(p graffiti-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa graffiti-gesture\n" + " key %a\n" + " =goal>\n" + " state %1S)", key); } } else if (type.equals(ActionType.Voice)) { String target = safeGlobalName(source); String utterance = ((VoiceAction) action).getText(); startVisit(step, "SAY"); addProperty(":target", target); emitProduction(step, true, "(p say-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?vocal>\n" + " state free\n" + "==>\n" + " !eval! %p\n" + " +vocal>\n" + " isa speak\n" + " string %a\n" + " =goal>\n" + " state %1S)", LispUtil.safeString(utterance)); emitProduction(step, false, "(p wait-for-speech-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?vocal>\n" + " state free\n" + "==>\n" + " !eval! %p\n" + // clear the aural-location buffer so we do // sit around waiting to hear ourselves next // time we try to listen to something! " -aural-location>\n" + " =goal>\n" + " state %1S)\n\n" + "(spp wait-for-speech-%c :at 0.0)"); } else if (type.equals(ActionType.Home)) { HomeAction homeAction = (HomeAction) action; if (homeAction.getHomeTarget() == HandLocation.OnKeyboard) { startVisit(step, "HOME-KEYBOARD"); addProperty(":target", "'keyboard"); emitProduction(step, false, "(p move-hand-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " state free\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa hand-to-home\n" + " =goal>\n" + " state %1S)"); handToHomePending = true; } else { startVisit(step, "HOME-MOUSE"); addProperty(":target", "'mouse"); emitProduction(step, false, "(p move-hand-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " state free\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa hand-to-mouse\n" + " =goal>\n" + " state %1S)"); } } else if (type.equals(ActionType.Tap)) { TapPressType pt = ((TapAction) action).getTapPressType(); int times = 1; if (pt.equals(TapPressType.DoubleTap)) { times = 2; } else if (pt.equals(TapPressType.TripleTap)) { times = 3; } boolean moveRequired = actAsMoveAndTap || ! cursorAtFocus(source); if (times > 1 || pt.equals(TapPressType.Tap)) { tap(step, moveRequired ? safeGlobalName(source) : null, times, moveRequired); } else { startVisit(step, "BUTTON-PRESS"); addProperty(":target", safeGlobalName(source)); addProperty(":use-finger", "t"); if (pt.equals(TapPressType.Down)) { emitProduction(step, true, "(p down-tap-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa finger-down\n" + " hand right\n" + " finger index\n" + " =goal>\n" + " state %1S)"); } else if (pt.equals(TapPressType.Up)) { emitProduction(step, true, "(p up-tap-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa finger-up\n" + " hand right\n" + " finger index\n" + " =goal>\n" + " state %1S)"); } } } else { throw new ComputationException("An unexpected Action type (" + type + ") was associated with an " + "ActionScriptStep. " + "Check the FSM."); } } // NOTE - the following is only here because older versions of // CogTool could create a TapScriptStep instead of an ActionScriptStep // with a tap or a TransitionScriptStep with a TapAction @Override public void visit(TapScriptStep step) { TapPressType tpt = ((TapAction) step.getAction()).getTapPressType(); int tapCount = 1; if (tpt == TapPressType.DoubleTap) { tapCount = 2; } else if (tpt != TapPressType.Tap) { throw new IllegalStateException( "Unknown TapPressType in old file: " + tpt); } startVisit(step, "TAP"); if (step.isMoveRequired()) { TransitionSource src = step.getStepFocus(); tap(step, safeGlobalName(src), tapCount, true); } else { tap(step, null, tapCount, false); } } // Expects target to be properly quoted. protected void tap(AScriptStep step, String target, int count, boolean moveRequired) { if (moveRequired) { emitProduction(step, false, "(p find-location-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?frame>\n" + " name %f\n" + " status visible\n" + "==>\n" + " !eval! %p\n" + " +visual-location>\n" + " isa visual-location\n" + " value %a\n" + " =goal>\n" + " state %s)\n\n" + "(spp find-location-%c :u -1.0)", target); addProperty(":use-finger", "t"); emitProduction(step, false, "(p move-finger-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " =visual-location>\n" + " isa visual-location\n" + " value %a\n" + " ?manual>\n" + " preparation free\n" + " execution free\n" + " state free\n" + " ?frame>\n" + " name %f\n" + " status visible\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa move-cursor\n" + " loc =visual-location\n" + " =visual-location>\n" + " =goal>\n" + " state %1S)", target); addProperty(":force-transition", "t"); emitProduction(step, (count == 1), "(p *suppress-re-encoding-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?visual>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + " status visible\n" + "==>\n" + " !eval! %p\n" + " +visual>\n" + " isa clear\n" + " =goal>\n" + " state %1S)" + "(spp *suppress-re-encoding-%c :at 0.0)"); --count; } while (count > 0) { if (target != null) { addProperty(":target", target); } addProperty(":use-finger", "t"); emitProduction(step, false, "(p prepare-tap-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " preparation free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa prepare\n" + " style punch\n" + " hand right\n" + " finger index\n" + " =goal>\n" + " state %1S)"); if (target != null) { addProperty(":target", target); } addProperty(":use-finger", "t"); emitProduction(step, (count == 1), "(p tap-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " execution free\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " +manual>\n" + " isa click-mouse\n" + " =goal>\n" + " state %1S)"); --count; } } @Override public void visit(DelayScriptStep step) { addProperty(":old-system-wait", Double.toString(step.getDelayDuration())); addProperty(":old-system-wait-label", LispUtil.safeString(step.getLabel())); emitProduction(step, false, "(p wait-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?frame>\n" + " name %f\n" + "==>\n" + " !eval! %p\n" + " =goal>\n" + " state %1s)\n\n" + "(spp find-%c :at 0.0)"); } @Override public void visit(TransitionDelayScriptStep step) { // do nothing on the ScriptStep; this is handled by the transition // information on the ScriptStep firing the transition } @Override public void visit(DriveScriptStep step) { throw new UnsupportedOperationException("Driving is not currently supported."); } @Override protected void generateLookAt(IWidget targetWidget, Frame destinationFrame) { Frame currentFrame = targetWidget.getFrame(); String target = safeGlobalName(targetWidget); startVisit(null, "LOOK-AT"); addProperty(":target", target); emitProduction(currentFrame, null, null, "(p find-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?frame>\n" + " name %f\n" + " status visible\n" + "==>\n" + " !eval! %p\n" + " +visual-location>\n" + " isa visual-location\n" + " value %a\n" + " =goal>\n" + " state %1s)\n\n" + "(spp find-%c :u -1.0)", target); addProperty(":target", target); emitProduction(currentFrame, destinationFrame, null, // TODO is this right? "(p look-at-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " =visual-location>\n" + " isa visual-location\n" + " value %a\n" + " ?visual>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + " status visible\n" + "==>\n" + " !eval! %p\n" + " +visual>\n" + " isa move-attention\n" + " screen-pos =visual-location\n" + " =visual-location>\n" + " =goal>\n" + " state %2s\n" + " suppress-re-encoding t)", target); addProperty(":target", target); emitProduction(currentFrame, destinationFrame, null, // TODO is this right? "(p look-at-%c\n" + " =goal>\n" + " isa klm\n" + " state %1s\n" + " =visual-location>\n" + " isa visual-location\n" + " value %a\n" + " ?visual>\n" + " state free\n" + " ?frame>\n" + " name %f\n" + " status visible\n" + "==>\n" + " !eval! %p\n" + " +visual>\n" + " isa move-attention\n" + " screen-pos =visual-location\n" + " =visual-location>\n" + " =goal>\n" + " state %2S" + " suppress-re-encoding t)", target); super.generateLookAt(targetWidget, destinationFrame); } @Override public void visit(LookAtScriptStep step) { generateLookAt(step.getLookAtTarget(), step.getDestinationFrame()); } @Override protected void generateMental(ThinkScriptStep step, double thinkDuration) { String duration = Double.toString(thinkDuration); String label = step.getLabel(); if (label == null || label.length() == 0) { label = "THINK"; } startVisit(null, LispUtil.safeString(label)); addProperty(":duration", duration); emitProduction(prevMental, false, "(p %0a-%c\n" + " =goal>\n" + " isa klm\n" + " state %s\n" + " ?manual>\n" + " state free\n" + "==>\n" + " !eval! %p\n" + " =goal>\n" + " state %1S)\n\n" + "(spp %0a-%c :at %1a)", LispUtil.makeSymbol(label), duration); } @Override public void visit(ThinkScriptStep step) { // May need to decrement the mental's duration if a look-at // is needed immediately following! registerMentalStep(step); } } }