/*
* File : Player.java
* Created : 18-dec-2000 10:21
* By : fbusquets
*
* JClic - Authoring and playing system for educational activities
*
* Copyright (C) 2000 - 2005 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;
/**
* <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 (fbusquets@xtec.cat)
* @version 13.09.10
*/
public class Player extends JPanel implements Constants, RunnableComponent, PlayStation, ActionListener {
// check fonts
//private static final Font CHECK_ARIAL=FontCheck.checkSystemFont("Arial", "arial2.ttf");
// 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(options.get(MEDIA_SYSTEM)!=null);
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.invalidate();
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;
if(fullPath.endsWith(".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{
fileSystem=new FileSystem(FileSystem.getPathPartOf(fullPath), thisPlayer);
projectName=FileSystem.getFileNameOf(fullPath);
}
// Set project
if(projectName.endsWith(".jclic")){
org.jdom.Document doc=fileSystem.getXMLDocument(projectName);
setProject(JClicProject.getJClicProject(doc.getRootElement(),
thisPlayer, fileSystem, fullPath));
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);
//act=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){
history.processJump(project.activitySequence.getJump(false, reporter), false);
}
};
actions[ACTION_PREV]=new AbstractAction(){
public void actionPerformed(ActionEvent ev){
history.processJump(project.activitySequence.getJump(true, reporter), false);
}
};
actions[ACTION_RETURN]=new AbstractAction(){
public void actionPerformed(ActionEvent ev){
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();
// ----- MODIFIED 24-Feb-2009
// Treat links to activity as links to sequence
// TODO: Check if this has secondary effects on existing JClic projects
// if(!navButtonsAlways)
// navButtonsDisabled=true;
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){
// 14-Mai-2010
// Correction of bug 1249: Incorrect reporting of activity results when maximum number of attempts achieved
// actPanel.finishActivity must be called when the action
// has been completely processed.
// The "solved" status should not be supposed to be always "false"
//actPanel.finishActivity(false);
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{
// Modified 21-Feb-2011
// getAppletContext().showDocument seems not working on a Mac
// (tested with Firefox and Safari)
// Instead, launch a new browser:
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;
}
}