/*
 * File    : Player.java
 * Created : 18-dec-2000 10:21
 * By      : fbusquets
 *
 * JClic - Authoring and playing system for educational activities
 *
 * Copyright (C) 2000 - 2018 Francesc Busquets & Departament
 * d'Educacio de la Generalitat de Catalunya
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details (see the LICENSE file).
 */

package edu.xtec.jclic;

import edu.xtec.jclic.bags.ActivityBagElement;
import edu.xtec.jclic.bags.ActivitySequenceElement;
import edu.xtec.jclic.bags.JumpInfo;
import edu.xtec.jclic.boxes.ActiveBox;
import edu.xtec.jclic.boxes.ActiveBoxContent;
import edu.xtec.jclic.boxes.BoxBase;
import edu.xtec.jclic.boxes.BoxConnector;
import edu.xtec.jclic.boxes.Counter;
import edu.xtec.jclic.clic3.Clic3;
import edu.xtec.jclic.fileSystem.FileSystem;
import edu.xtec.jclic.fileSystem.ZipFileSystem;
import edu.xtec.jclic.media.ActiveMediaBag;
import edu.xtec.jclic.media.ActiveMediaPlayer;
import edu.xtec.jclic.media.CheckMediaSystem;
import edu.xtec.jclic.media.EventSounds;
import edu.xtec.jclic.media.JavaSoundAudioBuffer;
import edu.xtec.jclic.media.MediaContent;
import edu.xtec.jclic.misc.Utils;
import edu.xtec.jclic.project.JClicProject;
import edu.xtec.jclic.report.Reporter;
import edu.xtec.jclic.skins.AboutWindow;
import edu.xtec.jclic.skins.Skin;
import edu.xtec.util.BrowserLauncher;
import edu.xtec.util.Html;
import edu.xtec.util.Messages;
import edu.xtec.util.Options;
import edu.xtec.util.ResourceManager;
import edu.xtec.util.StrUtils;
import java.applet.Applet;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.json.JSONObject;

/**
 * <CODE>Player</CODE> is one of the the main classes of the JClic system. It
 * implements the {@link edu.xtec.jclic.PlayStation} interface, so it can read
 * and play JClic projects from files or streams. In order to allow activities
 * to run, <CODE>Player</CODE> provides them of all the necessary resources:
 * media bags (to load and realize images and other media contents), sequence
 * control, report system management, user interface (loading and management of
 * skins), display of system messages, etc. Player is also a
 * {@link edu.xtec.jclic.RunnableComponent}, so it can be embedded in applets,
 * frames and other containers.
 *
 * @author Francesc Busquets ([email protected])
 * @version 13.09.10
 */
public class Player extends JPanel implements Constants, RunnableComponent, PlayStation, ActionListener {

  // static fields
  /** Name of the resource corresponding to the default skin of JClic. */
  public static final String DEFAULT_SKIN = "@default.xml";

  /** Name of the default reporter used by JClic. */
  public static final String DEFAULT_REPORTER = "Reporter";

  /**
   * List of the names of the internal resources corresponding to the default
   * event sounds used by JClic.
   */
  public static final String[] DEFAULT_EVENT_SOUNDS = {

      //
      // CHANGED 01-Mar-2013: Use sounds in WAV format to avoid openJDK errors
      //
      /* START */ "sounds/start.wav", /* CLICK */ "sounds/click.wav", /* ACTION_ERROR */ "sounds/action_error.wav",
      /* ACTION_OK */ "sounds/action_ok.wav", /* FINISHED_ERROR */ "sounds/finished_error.wav",
      /* FINISHED_OK */ "sounds/finished_ok.wav" };

  /**
   * List of the names of the internal resources corresponding to the default
   * icons associated to the basic actions defined in
   * {@link edu.xtec.jclic.Constants}.
   */
  public static final String[] ACTION_ICONS = { "icons/prev.gif", "icons/next.gif", "icons/return.gif",
      "icons/reset.gif", "icons/info_small.gif", "icons/help.gif", "icons/audio_on.gif", "icons/logo_button.gif" };

  /** The default name of the application (JClic) */
  public static final String DEFAULT_APP_NAME = "JClic";

  /**
   * Array containing the {@link javax.swing.Action} objects used by the player.
   */
  protected Action[] actions;

  /** The main {@link edu.xtec.util.Messages} object. */
  protected Messages messages;

  /**
   * The {@link edu.xtec.jclic.project.JClicProject} currently hosted by the
   * {@code Player}.
   */
  protected JClicProject project;

  /**
   * The UI element (<CODE>Panel</CODE>)of the <CODE>Activity</CODE> currently
   * running in the <CODE>
   * Player</CODE>.
   */
  protected Activity.Panel actPanel;

  /**
   * This object manages a list with the names of all the activities currently
   * played by the user in this <CODE>Player</CODE>.
   */
  protected PlayerHistory history;

  /** Current {@link edu.xtec.jclic.skins.Skin} of the <CODE>Player</CODE>. */
  protected Skin skin;

  /**
   * Default skin of this <CODE>Player</CODE>. Users can override the
   * <CODE>DEFAULT_SKIN</CODE> setting and choose another Skin to be used by
   * default in JClic.
   */
  protected Skin defaultSkin;

  /** Bag of realized media objects, ready to play. */
  protected ActiveMediaBag activeMediaBag;

  /** Current reporter used by this <CODE>Player</CODE>. */
  protected Reporter reporter;

  /** Current set of system sonds used in this <CODE>Player</CODE>. */
  protected EventSounds eventSounds;

  /**
   * Main <CODE>Timer</CODE>, used to feed the time conter. This timer generates
   * an <CODE>
   * ActionPerformed</CODE> event every second.
   */
  protected Timer timer;

  /**
   * This flag indicates if the <CODE>Player</CODE> must play the sounds
   * (including system sounds) and other media contents of the activities.
   */
  protected boolean audioEnabled = true;

  /**
   * This flag indicates if the program must write verbose info to the system
   * console.
   */
  protected boolean trace = false;

  /**
   * This flag indicates if the navigation buttons (<I>go to next activity</I> and
   * <I>go back</I>) are enabled o disabled.
   */
  protected boolean navButtonsDisabled = false;

  /**
   * When this flag is <CODE>true</CODE>, the navigation buttons are always
   * enabled, despite of the indications made by the activities or the sequence
   * control system. Used only for debugging projects with complicated sequence
   * chaining.
   */
  protected boolean navButtonsAlways = false;

  /**
   * The main name of the application in wich this <CODE>Player</CODE> is running.
   */
  protected String appName = DEFAULT_APP_NAME;

  Timer delayedTimer;
  Action delayedAction;
  Object currentConstraints;
  int[] counterVal = new int[NUM_COUNTERS];
  Cursor[] cursors = new Cursor[3];
  Options options;
  Image splashImg;
  Point bgImageOrigin = new Point();
  private edu.xtec.util.SwingWorker worker = null;

  /**
   * Creates a new <CODE>Player</CODE> object.
   *
   * @param options Options object to be used in the initialization process
   */
  public Player(Options options) {
    this(options, null);
  }

  /**
   * Creates a new <CODE>Player</CODE> object that will be initially loaded with a
   * specific <CODE>
   * JClicProject</CODE>.
   *
   * @param options Options object to be used in the initialization process
   * @param project JClic project to load (can be <CODE>null</CODE>)
   */
  public Player(Options options, JClicProject project) {
    this.options = options;
    this.project = project;
    init();
  }

  /**
   * Main initialization process, called once by constructors. Subclasses of
   * <CODE>Player</CODE> should override this method to initialize additional
   * members.
   */
  protected void init() {
    options.setLookAndFeel();
    CheckMediaSystem.check(options, false);
    setPreferredSize(new Dimension(600, 400));
    setLayout(null);
    Utils.checkRenderingHints(options);
    BoxConnector.checkOptions(options);
    ActiveBox.checkOptions(options);
    for (int i = 0; i < NUM_COUNTERS; i++)
      counterVal[i] = 0;
    setMessages();
    buildActions();
    setActionsText();
    history = new PlayerHistory(this);
    trace = options.getBoolean(TRACE);
    ActiveBox.compressImages = options.getBoolean(COMPRESS_IMAGES, true);
    audioEnabled = options.getBoolean(AUDIO_ENABLED, true);
    navButtonsAlways = options.getBoolean(NAV_BUTTONS_ALWAYS, false);
    setProject(project);
    activeMediaBag = new ActiveMediaBag();
    initSkin();
    setSkin(skin);
    setSystemMessage(getMessages().get("msg_initializing"), null);
    setWaitCursor(true);
    createCursors();
    createEventSounds();
    initTimers();
    splashImg = ResourceManager.getImageIcon(LOGO_ICON).getImage();
    if (skin != null && skin.hasMemMonitor())
      skin.setMem(Runtime.getRuntime().freeMemory());
    setWaitCursor(false);
    setWindowTitle();
    setSystemMessage(getMessages().get("msg_ready"), null);
  }

  /**
   * Starts the player, loading a specific project if specified. This method is
   * defined in the {@link RunnableComponent} interface.
   *
   * @param fullPath Full path to the JClic project file to be loaded. Can be
   *                 <I>null</I>.
   * @param sequence Optional parameter, used only when <CODE>fullPath</CODE> is
   *                 not <I>null</I>. It indicates the sequence where the to
   *                 start. It's also possible to indicate a string representation
   *                 of a number "N". In this case, the player will start with the
   *                 activity indicated by the Nth element of the main sequence of
   *                 the project.
   * @return <CODE>true</CODE> if the player starts successfully.
   *         <CODE>false</CODE> otherwise.
   */
  public boolean start(String fullPath, String sequence) {
    initReporter();
    if (fullPath != null)
      return load(fullPath, sequence);
    else
      return false;
  }

  /**
   * This method is called when the container gains the focus for the first time
   * or when losts it. Not used in <CODE>Player</CODE>.
   */
  public void activate() {
  }

  /** Instructs the RunnableComponent to stop working. */
  public void stop() {
    stopMedia(-1);
  }

  /** @throws Throwable */
  @Override
  protected void finalize() throws Throwable {
    try {
      end();
    } finally {
      super.finalize();
    }
  }

  /** Executes miscellaneous finalization routines. */
  public void end() {
    if (worker != null) {
      worker.interrupt();
      worker = null;
    }
    stopMedia();
    closeHelpWindow();
    if (actPanel != null) {
      actPanel.end();
      remove(actPanel);
      actPanel = null;
    }
    if (eventSounds != null) {
      eventSounds.close();
      eventSounds = null;
    }
    if (project != null) {
      project.end();
      project = null;
    }
    if (activeMediaBag != null)
      activeMediaBag.removeAll();
    if (reporter != null) {
      reporter.end();
      reporter = null;
    }
  }

  /** Creates and initializes the members of the {@link cursors} array. */
  protected void createCursors() {
    try {
      Toolkit tk = Toolkit.getDefaultToolkit();

      cursors[HAND_CURSOR] = tk.createCustomCursor(ResourceManager.getImageIcon("cursors/hand.gif").getImage(),
          new Point(8, 0), "hand");

      cursors[OK_CURSOR] = tk.createCustomCursor(ResourceManager.getImageIcon("cursors/ok.gif").getImage(),
          new Point(0, 0), "ok");

      cursors[REC_CURSOR] = tk.createCustomCursor(ResourceManager.getImageIcon("cursors/micro.gif").getImage(),
          new Point(15, 3), "record");

    } catch (Exception e) {
      System.err.println("Error creating cursor:\n" + e);
    }
  }

  /** Creates the {@link eventSounds} member and initializes the sound system. */
  protected void createEventSounds() {

    // Workaround for a JavaSound bug in Mac OS X
    if (options.getBoolean(Options.MAC)) {
      try {
        JavaSoundAudioBuffer.initialize();
      } catch (Exception ex) {
        System.err.println("Error initializing AudioBuffer lines:\n" + ex);
      }
    }
    // end of workaround

    eventSounds = new EventSounds(null);
    try {
      for (int i = 0; i < DEFAULT_EVENT_SOUNDS.length; i++) {
        String s = DEFAULT_EVENT_SOUNDS[i];
        eventSounds.setDataSource(i, ResourceManager.getResourceAsByteArray(s), options);
      }
      eventSounds.realize(options, project.mediaBag);
    } catch (Exception ex) {
      System.err.println("Error reading system sound:\"" + ex);
    }
    EventSounds.globalEnabled = options.getBoolean(SYSTEM_SOUNDS, true);
  }

  /** Creates and initializes the {@link reporter} member. */
  protected void initReporter() {
    if (reporter != null) {
      reporter.end();
      reporter = null;
    }
    String reporterClassName = StrUtils.secureString(options.getString(REPORTER_CLASS), DEFAULT_REPORTER);
    try {
      reporter = Reporter.getReporter(reporterClassName, options.getString(REPORTER_PARAMS), this, messages);
    } catch (Exception ex) {
      reporter = null;
      messages.showErrorWarning(this, "report_err_creating", reporterClassName, ex, null);
    }
  }

  /** Creates and initializes the {@link defaultSkin} member. */
  protected void initSkin() {
    String s = "";
    try {
      FileSystem fsSk = null;
      s = options.getString(SKIN);
      if (s == null)
        s = DEFAULT_SKIN;
      else if (!s.startsWith(Skin.INTERNAL_SKIN_PREFIX)) {
        fsSk = new FileSystem(FileSystem.getPathPartOf(s), this);
        s = FileSystem.getFileNameOf(s);
      }
      defaultSkin = Skin.getSkin(s, fsSk, this);
      actions[ACTION_REPORTS].setEnabled(true);
      actions[ACTION_AUDIO].setEnabled(true);
    } catch (Exception ex) {
      System.err.println("Error creating skin \"" + s + "\":\n" + ex);
    }
  }

  /**
   * Creates and initializes the members {@link timer}, {@link delayedTimer} and
   * {@link delayedAction}
   */
  protected void initTimers() {
    timer = new Timer(1000, this);
    delayedTimer = new Timer(1000, this);
    delayedTimer.setRepeats(false);
    delayedAction = null;
  }

  /** If open, closes the help dialog window. */
  public void closeHelpWindow() {
    if (skin != null) {
      if (skin.currentHelpWindow != null)
        skin.currentHelpWindow.setVisible(false);
      if (skin.currentAboutWindow != null)
        skin.currentAboutWindow.setVisible(false);
    }
  }

  /**
   * Creates and initializes the {@link messages} member.
   *
   * @return The <CODE>messages</CODE> member.
   */
  protected Messages setMessages() {
    messages = Messages.getMessages(options, DEFAULT_BUNDLE);
    messages.addBundle(COMMON_SETTINGS);
    setLocale(messages.getLocale());
    Locale.setDefault(messages.getLocale());
    setActionsText();
    if (skin != null) {
      skin.setLocale(messages.getLocale());
    }
    return messages;
  }

  public Component getTopComponent() {
    if (skin != null)
      return skin;
    return this;
  }

  public Skin getSkin() {
    return skin;
  }

  /** @param newSkin */
  public void setSkin(Skin newSkin) {
    if (newSkin == null)
      newSkin = defaultSkin;

    if (newSkin != null && !newSkin.equals(skin)) {
      Container top = null;
      Object[] currentSkinSettings = null;

      if (skin != null) {
        currentSkinSettings = skin.getCurrentSettings();
        skin.detach();
        top = skin.getParent();
        top.remove(skin);
      }

      newSkin.attach(this);
      skin = newSkin;

      if (top != null) {
        RootPaneContainer rpc = null;
        while (top != null && rpc == null) {
          if (top instanceof RootPaneContainer)
            rpc = (RootPaneContainer) top;
          else
            top = top.getParent();
        }

        if (rpc != null) {
          addTo(rpc, currentConstraints);
          top.validate();
          top.repaint();
        }
      }

      if (currentSkinSettings != null && skin != null)
        skin.setCurrentSettings(currentSkinSettings);
    }
  }

  public void addTo(RootPaneContainer cont, Object constraints) {
    currentConstraints = constraints;
    if (constraints == null) {
      cont.getContentPane().add(getTopComponent());
    } else {
      cont.getContentPane().add(getTopComponent(), constraints);
    }
  }

  protected FileSystem createFileSystem() {
    return new FileSystem(this);
  }

  protected void setProject(JClicProject p) {
    if (project != null) {
      if (project != p)
        project.end();
      removeActivity();
    }
    project = (p != null ? p : new JClicProject(this, createFileSystem(), null));
    project.realize(eventSounds, this);
    if (project.skin != null)
      defaultSkin = project.skin;
  }

  public boolean load(String fullPath, String sequence) {
    load(fullPath, sequence, null, null);
    return true;
  }

  public void load(final String sFullPath, final String sSequence, final String sActivity,
      final ActivityBagElement sAbe) {

    if (worker != null) {
      return;
    }

    worker = new edu.xtec.util.SwingWorker() {
      Activity.Panel actp;
      Exception exception = null;
      Player thisPlayer = Player.this;

      @Override
      public Object construct() {

        if (skin != null)
          skin.startAnimation();

        setWaitCursor(true);

        String fullPath = Clic3.pacNameToLowerCase(sFullPath);
        String sequence = Clic3.pacNameToLowerCase(sSequence);
        Activity act = null;
        String activityName = sActivity;
        ActivityBagElement abe = sAbe;
        FileSystem fileSystem = project.getFileSystem();

        try {
          // Step 1: load or create project and set a valid value for "sequence"
          if (fullPath != null) {
            setSystemMessage(messages.get("msg_loading_project"), FileSystem.getFileNameOf(fullPath));
            if (sequence == null)
              sequence = "0";

            // Check fileSystem and projectName
            if (fileSystem != null) {
              fullPath = fileSystem.getUrl(fullPath);
              if (fullPath.startsWith("file://"))
                fullPath = fullPath.substring(7);
              // Added 03-Feb-2011
              // Remove trailing parameters of URLs
              else if (fullPath.indexOf('?') > 0)
                fullPath = fullPath.substring(0, fullPath.indexOf('?'));
              // ----------
            }

            String projectName = null;
            JSONObject json = null;
            if (fullPath.endsWith(Utils.EXT_JCLIC_ZIP)) {
              fileSystem = FileSystem.createFileSystem(fullPath, thisPlayer);
              String[] projects = ((ZipFileSystem) fileSystem).getEntries(".jclic");
              if (projects == null)
                throw new Exception("File " + fullPath + " does not contain any jclic project");
              projectName = projects[0];
            } else if (fullPath.endsWith(Utils.EXT_SCORM_ZIP)) {
              fileSystem = FileSystem.createFileSystem(fullPath, thisPlayer);
              if (fileSystem.fileExists("project.json")) {
                json = new JSONObject(new String(fileSystem.getBytes("project.json")));
                projectName = json.optString("mainFile", null);
              }
              if (projectName == null)
                throw new Exception("Invalid JClic SCORM file: " + fullPath);
            } else {
              fileSystem = new FileSystem(FileSystem.getPathPartOf(fullPath), thisPlayer);
              projectName = FileSystem.getFileNameOf(fullPath);
              if (fileSystem.fileExists("project.json"))
                json = new JSONObject(new String(fileSystem.getBytes("project.json")));
            }

            // Set project
            if (projectName.endsWith(".jclic")) {
              org.jdom.Document doc = fileSystem.getXMLDocument(projectName);
              JClicProject prj = JClicProject.getJClicProject(doc.getRootElement(), thisPlayer, fileSystem, fullPath);
              if (json != null)
                prj.readJSON(json, false);
              setProject(prj);
              if (reporter != null)
                reporter.newSession(project, thisPlayer, messages);
            } else {
              sequence = projectName;
              setProject(new JClicProject(thisPlayer, fileSystem, fullPath));
            }
          }

          // Step 2: load ActivitySequenceElement ase
          if (sequence != null) {
            String seqName = FileSystem.stdFn(sequence);
            setSystemMessage(messages.get("msg_loading_project"), FileSystem.getFileNameOf(seqName));

            navButtonsDisabled = false;
            ActivitySequenceElement ase = project.activitySequence.getElementByTag(seqName, true);

            // if sequence does no exists, get existing sequence by number
            if (ase == null) {
              int i = StrUtils.getAbsIntValueOf(seqName);
              if (i >= 0)
                ase = project.activitySequence.getElement(i, true);
            }

            // at this point, if ase==null the sequence was not found in project.
            // try load new sequence (only with Clic3 files)
            if (ase == null) {
              boolean firstPac = (project.activitySequence.getSize() == 0);
              boolean isPcc = seqName.endsWith(".pcc");
              boolean isPac = seqName.endsWith(".pac");
              if (isPcc || isPac) {
                if (isPcc) {
                  String path = fileSystem.root + seqName;
                  fileSystem = FileSystem.createFileSystem(path, thisPlayer);
                  if (firstPac) {
                    project.setFileSystem(fileSystem);
                    project.setFullPath(path);
                  } else
                    setProject(new JClicProject(thisPlayer, fileSystem, path));
                  firstPac = true;
                  Clic3.readPccFile(project);
                  ase = project.activitySequence.getCurrentAct();
                } else if (isPac) {
                  Clic3.addPacToSequence(project, seqName);
                  ase = project.activitySequence.getElementByTag(seqName, true);
                }

                if (firstPac) {
                  project.setName(seqName);
                  if (reporter != null)
                    reporter.newSession(project, thisPlayer, messages);
                }
              }
            }

            if (ase != null) {
              if (reporter != null)
                reporter.newSequence(ase);
              activityName = ase.getActivityName();
            }
          }

          // step 3: load ActivityBagElement abe
          if (activityName != null) {
            String actName = FileSystem.stdFn(activityName);
            abe = project.activityBag.getElement(actName);
          }

          // step 4: load Activity act
          if (abe != null) {
            setSystemMessage(messages.get("msg_loading_activity"), abe.getName());
            act = Activity.getActivity(abe.getData(), project);
          }

          // step 5: Load activity
          if (act != null) {
            setSystemMessage(null, messages.get("msg_preparing_media"));
            if (project.settings.eventSounds != null)
              act.eventSounds.setParent(project.settings.eventSounds);
            project.mediaBag.waitForAllImages();
            act.prepareMedia(thisPlayer);
            activeMediaBag.realizeAll();
            if (abe != null)
              project.activitySequence.checkCurrentActivity(abe.getName());
            setSystemMessage(null, messages.get("msg_initializing"));
            actp = act.getActivityPanel(thisPlayer);
            actp.buildVisualComponents();
          }
        } catch (Exception ex) {
          exception = ex;
          if (project == null)
            setProject(null);
          actp = null;
        }
        return actp;
      }

      @Override
      public void finished() {

        setWaitCursor(false);

        if (actPanel != null) {
          actPanel.end();
          remove(actPanel);
          actPanel = null;
          setCounterValue(TIME_COUNTER, 0);
        }

        if (actp != null && worker != null) {
          // moved to thread
          setBackgroundSettings(actp.getActivity());
          add(actPanel = actp);
          actPanel.setCursor(null);
          splashImg = null;

          // set skin
          if (skin != null)
            skin.resetAllCounters(false);

          if (actp.skin != null)
            setSkin(actp.skin);
          else if (project.skin != null)
            setSkin(project.skin);
          else
            setSkin(defaultSkin);

          if (skin != null) {
            boolean hasReturn = (history.storedElementsCount() > 0);
            int navBtnFlag = navButtonsAlways ? ActivitySequenceElement.NAV_BOTH
                : navButtonsDisabled ? ActivitySequenceElement.NAV_NONE : project.activitySequence.getNavButtonsFlag();

            if (actions != null) {
              actions[ACTION_NEXT].setEnabled((navBtnFlag & ActivitySequenceElement.NAV_FWD) != 0
                  && project.activitySequence.hasNextAct(hasReturn));
              actions[ACTION_PREV].setEnabled((navBtnFlag & ActivitySequenceElement.NAV_BACK) != 0
                  && project.activitySequence.hasPrevAct(hasReturn));
              actions[ACTION_RETURN].setEnabled(history.storedElementsCount() > 0);
              actions[ACTION_HLP].setEnabled(actp.getActivity().helpWindowAllowed());
              actions[ACTION_RESET].setEnabled(actp.getActivity().canReinit());
              actions[ACTION_INFO].setEnabled(actp.getActivity().hasInfo());
            }
          }
          // place activity on screen
          setSystemMessage(messages.get("msg_ready"), null);
          initActivity();
        } else if (exception != null) {
          String sType = null;
          List<Object> v = new ArrayList<Object>();
          if (sFullPath != null) {
            v.add(sFullPath);
            sType = "msg_error_loading_project";
          }
          if (sSequence != null) {
            v.add(sSequence);
            if (sType == null)
              sType = "msg_error_loading_sequence";
          }
          if (sActivity != null) {
            v.add(sActivity);
            if (sType == null)
              sType = "msg_error_loading_activity";
          }
          if (sAbe != null) {
            v.add(sAbe.getName());
            if (sType == null)
              sType = "msg_error_loading_activity";
          }
          if (sType == null)
            sType = Messages.ERROR;

          setSystemMessage(messages.get(sType), null);
          messages.showErrorWarning(thisPlayer, "err_reading_data", v, exception, null);

          validate();
        } else {
          setSystemMessage(messages.get("msg_ready"), null);
        }

        // unlock events
        setWindowTitle();
        worker = null;
        if (skin != null) {
          skin.stopAnimation();
          skin.setEnabled(true);
        }
        setEnabled(true);
      }
    };

    // Main thread, after SwingWorker was build:
    forceFinishActivity();
    if (skin != null)
      skin.setEnabled(false);
    setEnabled(false);
    worker.start();
  }

  public void forceFinishActivity() {
    if (timer != null) {
      timer.stop();
      delayedTimer.stop();
      if (actPanel != null) {
        closeHelpWindow();
        actPanel.forceFinishActivity();
        stopMedia();
        activeMediaBag.removeAll();
        if (Utils.lowMemoryCondition()) {
          if (trace)
            System.out.println(">>> LOW MEMORY! cleaning...");
          project.mediaBag.clearData();
          System.runFinalization();
          System.gc();
        }
      }
      setCursor(null);
    }
  }

  public void removeActivity() {
    forceFinishActivity();
    if (actPanel != null) {
      actPanel.end();
      remove(actPanel);
      setMsg(null);
      setBackgroundSettings(null);
      actPanel = null;
    }
  }

  public void initActivity() {

    setWaitCursor(true);
    setCursor(null);
    timer.stop();
    delayedTimer.stop();
    setCounterValue(TIME_COUNTER, 0);
    stopMedia();
    try {
      if (actPanel != null) {
        actPanel.initActivity();
        timer.start();
        if (!actPanel.getActivity().mustPauseSequence())
          startAutoPassTimer();
        if (getFressa() != null)
          getFressa().initActivity(actPanel);
        setSystemMessage(messages.get("msg_activity_running"), null);
      }
      if (skin != null)
        skin.setMem(Runtime.getRuntime().freeMemory());
    } catch (Exception ex) {
      messages.showErrorWarning(this, "msg_error_starting_activity", ex);
      setSystemMessage(messages.get("ERROR"), null);
    } finally {
      setWaitCursor(false);
      validate();
      repaint();
    }
  }

  public void startActivity(Activity.Panel ap) {
    setWaitCursor(true);
    try {
      ap.startActivity();
    } catch (Exception ex) {
      messages.showErrorWarning(this, "msg_error_starting_activity", ex);
      setSystemMessage(messages.get("ERROR"), null);
    } finally {
      setWaitCursor(false);
    }
  }

  @Override
  public void doLayout() {
    if (trace)
      System.out.println(">>> layout!");
    if (actPanel != null) {
      BoxBase.resetAllFonts();
      Rectangle bounds = getBounds();
      Rectangle proposedRect = new Rectangle(AC_MARGIN, AC_MARGIN, bounds.width - 2 * AC_MARGIN,
          bounds.height - 2 * AC_MARGIN);
      if (actPanel.bgImage != null && !actPanel.getActivity().tiledBgImg) {
        bgImageOrigin.x = (getWidth() - actPanel.bgImage.getWidth(this)) / 2;
        bgImageOrigin.y = (getHeight() - actPanel.bgImage.getHeight(this)) / 2;
        if (actPanel.getActivity().absolutePositioned) {
          proposedRect.x = bgImageOrigin.x;
          proposedRect.y = bgImageOrigin.y;
          proposedRect.width -= (bgImageOrigin.x - AC_MARGIN);
          proposedRect.height -= (bgImageOrigin.y - AC_MARGIN);
          proposedRect.width = Math.min(proposedRect.width, bounds.width);
          proposedRect.height = Math.min(proposedRect.height, bounds.height);
        }
      }
      actPanel.fitTo(proposedRect, bounds);
    }
  }

  @Override
  public void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g;

    if (splashImg != null) {
      int x, y, imgW, imgH;
      g2.setColor(BG_COLOR);
      g2.fill(g2.getClip());
      imgW = splashImg.getWidth(this);
      imgH = splashImg.getHeight(this);
      x = (getBounds().width - imgW) / 2;
      y = (getBounds().height - imgH) / 2;
      g2.drawImage(splashImg, x, y, this);
      return;
    }

    Rectangle rBounds = new Rectangle(0, 0, getWidth(), getHeight());

    if (actPanel == null || actPanel.getActivity().bgGradient == null
        || actPanel.getActivity().bgGradient.hasTransparency())
      super.paintComponent(g);

    if (actPanel != null && (actPanel.getActivity().bgGradient != null || actPanel.bgImage != null)) {
      RenderingHints rh = g2.getRenderingHints();
      g2.setRenderingHints(DEFAULT_RENDERING_HINTS);

      if (actPanel.getActivity().bgGradient != null)
        actPanel.getActivity().bgGradient.paint(g2, rBounds);

      if (actPanel.bgImage != null) {
        Rectangle r = new Rectangle(0, 0, actPanel.bgImage.getWidth(this), actPanel.bgImage.getHeight(this));
        Rectangle gBounds = g2.getClipBounds();

        if (!actPanel.getActivity().tiledBgImg) {
          r.setLocation(bgImageOrigin);
          if (r.intersects(gBounds)) {
            g2.drawImage(actPanel.bgImage, bgImageOrigin.x, bgImageOrigin.y, this);
          }
        } else {
          Utils.tileImage(g2, actPanel.bgImage, rBounds, r, this);
        }
      }
      g2.setRenderingHints(rh);
    }
  }

  // Methods inherited from interface ActionListener
  public void actionPerformed(ActionEvent e) {
    String ac = null;
    if (timer != null && e.getSource().equals(timer)) {
      incCounterValue(TIME_COUNTER);
      if (actPanel != null && actPanel.getActivity().maxTime > 0 && actPanel.isPlaying()
          && counterVal[TIME_COUNTER] >= actPanel.getActivity().maxTime) {
        actPanel.finishActivity(false);
      }
      return;
    }

    if (delayedTimer != null && e.getSource().equals(delayedTimer)) {
      delayedTimer.stop();
      if (delayedAction != null) {
        delayedAction.actionPerformed(null);
      }
    }

    if (ac == null && (ac = e.getActionCommand()) == null)
      return;
    delayedAction = null;

    processActionEvent(ac);
  }

  protected int getNumActions() {
    return NUM_ACTIONS;
  }

  protected void buildActions() {

    actions = new Action[getNumActions()];

    actions[ACTION_NEXT] = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        if(history != null && project != null && project.activitySequence != null)
          history.processJump(project.activitySequence.getJump(false, reporter), false);
      }
    };

    actions[ACTION_PREV] = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        if(history != null && project != null && project.activitySequence != null)
          history.processJump(project.activitySequence.getJump(true, reporter), false);
      }
    };

    actions[ACTION_RETURN] = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        if(history != null)
          history.pop();
      }
    };

    actions[ACTION_RESET] = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        if (actPanel != null && actPanel.getActivity().canReinit())
          initActivity();
      }
    };

    actions[ACTION_HLP] = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        if (actPanel != null)
          actPanel.showHelp();
      }
    };

    actions[ACTION_INFO] = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        if (actPanel != null && actPanel.getActivity().hasInfo()) {
          if (actPanel.getActivity().infoUrl != null) {
            displayUrl(actPanel.getActivity().infoUrl, true);
          } else if (actPanel.getActivity().infoCmd != null) {
            runCmd(actPanel.getActivity().infoCmd);
          }
        }
      }
    };

    actions[ACTION_REPORTS] = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        showAbout(true);
      }
    };

    actions[ACTION_AUDIO] = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        Object vBack = getValue(AbstractAction.DEFAULT);
        audioEnabled = !audioEnabled;
        Object vNew = audioEnabled ? Boolean.TRUE : Boolean.FALSE;
        if (!audioEnabled) {
          stopMedia();
          EventSounds.globalEnabled = false;
        } else {
          EventSounds.globalEnabled = options.getBoolean(SYSTEM_SOUNDS, true);
        }
        putValue(AbstractAction.DEFAULT, vNew);
        if (changeSupport != null) {
          PropertyChangeEvent evt = new PropertyChangeEvent(this, "selected", vBack, vNew);
          changeSupport.firePropertyChange(evt);
        }
      }
    };
    actions[ACTION_AUDIO].putValue(AbstractAction.DEFAULT, audioEnabled ? Boolean.TRUE : Boolean.FALSE);

    for (int dynAct : DYNAMIC_ACTIONS) {
      actions[dynAct].setEnabled(false);
    }
    actions[ACTION_AUDIO].setEnabled(true);
  }

  protected void setActionsText() {
    if (actions != null) {
      for (int i = 0; i < actions.length; i++) {
        if (actions[i] != null) {
          String s = messages.get("action_" + getActionName(i) + "_caption");
          if (!s.equals(actions[i].getValue(Action.NAME)))
            actions[i].putValue(Action.NAME, s);
          s = messages.get("action_" + getActionName(i) + "_tooltip");
          if (!s.equals(actions[i].getValue(Action.SHORT_DESCRIPTION)))
            actions[i].putValue(Action.SHORT_DESCRIPTION, s);
          s = messages.get("action_" + getActionName(i) + "_keys");
          if (s != null && s.length() == 2) {
            actions[i].putValue(Action.MNEMONIC_KEY, new Integer(s.charAt(0)));
            char c = s.charAt(1);
            int kk = -1;
            if (c == '*') {
              switch (i) {
              case ACTION_NEXT:
                kk = KeyEvent.VK_RIGHT;
                break;

              case ACTION_PREV:
                kk = KeyEvent.VK_LEFT;
                break;

              case ACTION_RETURN:
                kk = KeyEvent.VK_UP;
                break;

              case ACTION_RESET:
                kk = KeyEvent.VK_ENTER;
                break;

              default:
                break;
              }
            } else
              kk = (int) c;

            if (kk >= 0)
              actions[i].putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(kk, KeyEvent.CTRL_MASK));
          }
          Icon icon = getActionIcon(i);
          if (icon != null && !icon.equals(actions[i].getValue(Action.SMALL_ICON)))
            actions[i].putValue(Action.SMALL_ICON, icon);
        }
      }
    }
  }

  protected String getActionName(int actionId) {
    if (actionId < 0 || actionId >= ACTION_NAME.length)
      return null;
    return ACTION_NAME[actionId];
  }

  protected Icon getActionIcon(int actionId) {
    if (actionId < 0 || actionId >= ACTION_ICONS.length)
      return null;
    return ResourceManager.getImageIcon(ACTION_ICONS[actionId]);
  }

  public Action getAction(int id) {
    if (actions == null || id < 0 || id >= actions.length)
      return null;
    return actions[id];
  }

  protected boolean processActionEvent(String ac) {
    return !isEnabled();
  }

  protected void showAbout(boolean selectReportPane) {
    if (skin != null) {
      AboutWindow aw = skin.buildAboutWindow();
      try {
        aw.buildAboutTab("JClic", getMsg("JCLIC_VERSION"), null, null, null, null, null);
        aw.buildStandardTab(aw.getHtmlSystemInfo(), "about_window_systemInfo", "about_window_lb_system",
            "icons/system_small.gif");
        if (project != null) {
          StringBuilder sb = new StringBuilder(4096);
          sb.append(project.settings.toHtmlString(messages));
          if (actPanel != null) {
            sb.append(Html.BR).append(actPanel.getActivity().toHtmlString(this));
          }
          aw.buildStandardTab(sb.substring(0), "about_window_projectInfo", "about_window_lb_project",
              "icons/info_small.gif");
        }
        if (reporter != null) {
          aw.buildStandardTab(reporter.toHtmlString(messages), "about_window_reportInfo", "about_window_lb_report",
              "icons/report_small.gif");
          if (selectReportPane)
            aw.getTabbedPane().setSelectedIndex(3);
        }
        skin.showAboutWindow(aw);

      } catch (Exception ex) {
        System.err.println("Error building about window!\n" + ex);
      }
    }
  }

  // Methods inherited from interface ActivityContainer
  public void playMedia(final MediaContent mediaContent, final ActiveBox mediaPlacement) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        String s = mediaContent.mediaFileName;
        JumpInfo ji;

        switch (mediaContent.mediaType) {
        case MediaContent.RUN_CLIC_PACKAGE:
          ji = new JumpInfo(JumpInfo.JUMP, FileSystem.stdFn(s));
          ji.projectPath = mediaContent.externalParam;
          history.processJump(ji, true);
          break;
        case MediaContent.RUN_CLIC_ACTIVITY:
          history.push();
          load(null, null, s, null);
          break;
        case MediaContent.RETURN:
          history.pop();
          break;
        case MediaContent.EXIT:
          String exitUrl = mediaContent.mediaFileName;
          if (exitUrl != null)
            exitUrl = project.getFileSystem().getUrl(exitUrl);
          ji = new JumpInfo(JumpInfo.EXIT, exitUrl);
          history.processJump(ji, false);
          break;
        case MediaContent.RUN_EXTERNAL:
          if (mediaContent.mediaFileName != null) {
            StringBuilder sb = new StringBuilder(mediaContent.mediaFileName);
            if (mediaContent.externalParam != null)
              sb.append(" ").append(mediaContent.externalParam);
            runCmd(sb.substring(0));
          }
          break;
        case MediaContent.URL:
          if (mediaContent.mediaFileName != null) {
            displayUrl(mediaContent.mediaFileName, true);
          }
          break;
        case MediaContent.PLAY_AUDIO:
        case MediaContent.PLAY_MIDI:
        case MediaContent.PLAY_VIDEO:
        case MediaContent.RECORD_AUDIO:
        case MediaContent.PLAY_RECORDED_AUDIO:
          if (audioEnabled) {
            ActiveMediaPlayer amp = activeMediaBag.getActiveMediaPlayer(mediaContent, project.mediaBag, Player.this);
            if (amp != null) {
              amp.play(mediaPlacement);
            }
          }
          break;
        default:
          break;
        }
      }
    });
  }

  protected void runCmd(String cmd) {
    if (options.get(Options.APPLET) != null) {
      messages.showAlert(this, "msg_warn_no_exec_in_applets");
      return;
    }

    try {
      Runtime.getRuntime().exec(cmd, null, new File(project.getFileSystem().root));
    } catch (Exception ex) {
      messages.showErrorWarning(this, "msg_error_executing_external", cmd, ex, null);
    }
  }

  public void stopMedia() {
    stopMedia(-1);
  }

  public void stopMedia(int level) {
    activeMediaBag.stopAll(level);
  }

  public void activityFinished(boolean completedOk) {
    closeHelpWindow();

    if (getFressa() != null)
      getFressa().activityFinished();

    if (completedOk) {
      setCursor(getCustomCursor(OK_CURSOR));
      actPanel.setCursor(null);
    }
    setSystemMessage(messages.get("msg_activity_finished"), null);
    timer.stop();
    startAutoPassTimer();
  }

  public void startAutoPassTimer() {
    ActivitySequenceElement ase = project.activitySequence.getCurrentAct();
    if (ase != null && ase.delay > 0 && !delayedTimer.isRunning() && !navButtonsDisabled) {
      delayedAction = actions[ACTION_NEXT];
      delayedTimer.setInitialDelay(ase.delay * 1000);
      delayedTimer.start();
    }
  }

  protected void setBackgroundSettings(Activity act) {
    setBackground(act != null ? act.bgColor : Color.lightGray);
    bgImageOrigin.setLocation(0, 0);
    repaint();
  }

  public void setMsg(ActiveBoxContent abc) {
    ActiveBox ab = null;
    if (skin != null)
      ab = skin.getMsgBox();
    if (ab != null) {
      ab.clear();
      ab.setContent(abc == null ? ActiveBoxContent.getEmptyContent() : abc);
    }
  }

  public void playMsg() {
    if (skin != null && skin.getMsgBox() != null) {
      skin.getMsgBox().playMedia(this);
    }
  }

  public void incCounterValue(int counterId) {
    counterVal[counterId]++;
    Counter c = null;
    if (skin != null && (c = skin.getCounter(counterId)) != null)
      c.setValue(counterVal[counterId]);
    if (counterId == ACTIONS_COUNTER && actPanel != null && actPanel.getActivity().maxActions > 0
        && actPanel.isPlaying() && counterVal[ACTIONS_COUNTER] >= actPanel.getActivity().maxActions) {
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          actPanel.finishActivity(actPanel.solved);
        }
      });
    }
  }

  public void setCountDown(int counterId, int maxValue) {
    Counter c = null;
    if (skin != null && (c = skin.getCounter(counterId)) != null)
      c.setCountDown(maxValue);
  }

  public void setCounterValue(int counterId, int newValue) {
    counterVal[counterId] = newValue;
    Counter c = null;
    if (skin != null && (c = skin.getCounter(counterId)) != null)
      c.setValue(newValue);
  }

  public int getCounterValue(int counterId) {
    return counterVal[counterId];
  }

  public void setCounterEnabled(int counterId, boolean bEnabled) {
    if (skin != null) {
      skin.enableCounter(counterId, bEnabled);
      setCountDown(counterId, 0);
    }
  }

  public Messages getMessages() {
    return (messages == null ? setMessages() : messages);
  }

  public void setWaitCursor(boolean state) {
    if (skin != null) {
      skin.setWaitCursor(state);
    }
  }

  public void setSystemMessage(String msg1, String msg2) {

    if (skin != null)
      skin.setSystemMessage(msg1, msg2);

    if (trace)
      System.out.println("MSG " + (msg1 == null ? "" : msg1 + " ") + (msg2 == null ? "" : msg2));
  }

  public JComponent getComponent() {
    return this;
  }

  public ActiveMediaPlayer getActiveMediaPlayer(MediaContent mediaContent) {
    if (activeMediaBag != null && mediaContent != null)
      return activeMediaBag.getActiveMediaPlayer(mediaContent, project.mediaBag, this);
    else
      return null;
  }

  /**
   * Gets the custom cursor corresponding to the indicated type
   *
   * @param type Type of cursor.
   * @return The requested cursor, or the default system cursor if not found.
   */
  public Cursor getCustomCursor(int type) {
    if (type >= 0 && type < cursors.length)
      return cursors[type];
    else
      return null;
  }

  public void reportNewActivity(Activity act, int currentScore) {
    ActivitySequenceElement ase = project.activitySequence.getCurrentAct();
    if (reporter != null) {
      if (ase.getTag() != null && !ase.getTag().equals(reporter.getCurrentSequenceTag()))
        reporter.newSequence(ase);
      if (act.includeInReports)
        reporter.newActivity(act);
    }
    setCounterValue(ACTIONS_COUNTER, 0);
    setCounterValue(SCORE_COUNTER, 0);
  }

  public void reportNewAction(Activity act, String type, String source, String dest, boolean ok, int currentScore) {
    if (reporter != null && act.includeInReports && act.reportActions)
      reporter.newAction(type, source, dest, ok);

    if (currentScore >= 0) {
      incCounterValue(ACTIONS_COUNTER);
      setCounterValue(SCORE_COUNTER, currentScore);
    }
  }

  public void reportEndActivity(Activity act, boolean solved) {
    if (reporter != null && act.includeInReports)
      reporter.endActivity(counterVal[SCORE_COUNTER], counterVal[ACTIONS_COUNTER], solved);
  }

  public boolean showHelp(JComponent hlpComponent, String hlpMsg) {
    if (skin != null) {
      skin.showHelp(hlpComponent, hlpMsg);
      return true;
    }
    return false;
  }

  public InputStream getProgressInputStream(InputStream is, int expectedLength, String name) {
    if (skin != null && is != null && !(is instanceof ByteArrayInputStream)) {
      is = skin.getProgressInputStream(is, expectedLength, name);
    }
    return is;
  }

  public Options getOptions() {
    return options;
  }

  public PlayerHistory getHistory() {
    return history;
  }

  public void displayUrl(String url, boolean inFrame) {
    if (url != null) {
      url = project.getFileSystem().getUrl(url);
      try {
        displayUrl(new URL(url), inFrame);
      } catch (Exception ex) {
        System.err.println("Unable to invoque URL " + url + "\n" + ex);
      }
    }
  }

  public void displayUrl(URL url, boolean inFrame) {
    if (url == null)
      return;
    Applet applet = options.getApplet();
    try {
      if (applet != null && !options.getBoolean(Options.MAC)) {
        if (inFrame) {
          String frame = (String) options.get(INFO_URL_FRAME);
          if (frame == null)
            frame = "_BLANK";
          applet.getAppletContext().showDocument(url, frame);
        } else {
          end();
          applet.getAppletContext().showDocument(url);
        }
      } else {
        BrowserLauncher.openURL(url.toExternalForm());
      }
    } catch (Exception ex) {
      System.err.println("Unable to invoque URL " + url + "\n" + ex);
    }
  }

  public void exit() {
    exit(null);
  }

  public void exit(String url) {
    final String sUrl = (url == null ? options.getString(EXIT_URL) : url);
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        if (sUrl != null) {
          displayUrl(sUrl, false);
        }
        if (options.getApplet() == null) {
          try {
            end();
            Frame fr = JOptionPane.getFrameForComponent(getTopComponent());
            if (fr != null)
              fr.dispose();
            else
              System.exit(0);
          } catch (Exception ex) {
            System.err.println("Unable to exit!\n" + ex);
          }
        }
      }
    });
  }

  @Override
  public void requestFocus() {
    if (actPanel != null)
      actPanel.requestFocus();
  }

  public String getMsg(String key) {
    return messages.get(key);
  }

  public void doAutoStart() {
  }

  public boolean newInstanceRequest(final String param1, final String param2) {
    boolean result = false;
    if (param1 != null) {
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          Frame frame = JOptionPane.getFrameForComponent(Player.this);
          if (frame != null)
            frame.toFront();
          load(param1, param2);
        }
      });
      result = true;
    }
    return result;
  }

  public boolean windowCloseRequested() {
    return true;
  }

  public void setWindowTitle(String docTitle) {
    Window w = Options.getWindowForComponent(this);
    if (w != null) {
      StringBuilder sb = new StringBuilder();
      String s = StrUtils.nullableString(docTitle);
      if (s != null)
        sb.append(s).append(" - ");
      sb.append(appName);
      if (w instanceof Frame)
        ((Frame) w).setTitle(sb.substring(0));
      else if (w instanceof Dialog)
        ((Dialog) w).setTitle(sb.substring(0));
    }
  }

  public void setWindowTitle() {
    StringBuilder sb = new StringBuilder();
    String prjName = project == null ? null : StrUtils.nullableString(project.getPublicName());
    String actName = actPanel == null ? null : StrUtils.nullableString(actPanel.getActivity().getPublicName());
    if (actName != null) {
      sb.append(actName);
      if (prjName != null)
        sb.append(" [");
    }
    if (prjName != null) {
      sb.append(prjName);
      if (actName != null)
        sb.append("]");
    }
    setWindowTitle(sb.substring(0));
  }

  /**
   * FressaFunctions offers special accessibility features like atomatic scanning
   * and voice synthesis.
   *
   * @return The FressaFunctions object, or <CODE>null</CODE> if accessibility
   *         features are not enabled
   */
  public edu.xtec.jclic.accessibility.FressaFunctions getFressa() {
    return null;
  }
}