#include "lenv.h"
#include "lpackage.h"
#include "filesandpaths.h"
#include <QMap>
#include <QTemporaryFile>
#include "flexibleevaluator.h"
#ifdef QT_GUI_LIB
#include <QDesktopServices>
#endif // QT_GUI_LIB
LurchEnvironment::LurchEnvironment ()
: LurchPathTracker(), dirty( false ), interp( NULL ), undoMerge( false ),
watchState( PreventChanges ), invisibleActions( false ), numTempDepsLoaded( 0 ),
titleWasAutoGenerated( false ), busy( false )
{
// index this object
int i = environments.indexOf( NULL );
if ( i == -1 )
environments << this;
else
environments[i] = this;
// now do the real work of the constructor
prepareNewDocument();
connect( &doc, SIGNAL(modified(const LobChange&)),
this, SLOT(watchDocumentChanges(const LobChange&)) );
newInterpreter();
}
LurchEnvironment::~LurchEnvironment ()
{
int i = index();
if ( i > -1 ) // which should always be true, but let's be safe
environments[i] = NULL;
if ( interp != NULL )
delete interp;
foreach ( LurchPackage* package, urn2pkg )
delete package;
}
Lob LurchEnvironment::document ()
{
return doc;
}
QScriptEngine* LurchEnvironment::engine ()
{
return interp;
}
void LurchEnvironment::prepareNewDocument ()
{
doc.prepareModifiedSignal();
currentDocumentURN = doc.getURN();
currentDocumentDependencies = doc.getDependencies();
undoStack.clear();
redoStack.clear();
emit undoAvailableChanged( false );
emit redoAvailableChanged( false );
}
QStringList LurchEnvironment::safeToAddNicknames ( Lob thisTree ) const
{
QStringList result;
if ( !thisTree.checkUniqueNicknames() ) {
QStringList tmp = thisTree.listAllLabels( "nickname" );
while ( tmp.count() ) {
QString first = tmp.takeFirst();
if ( tmp.contains( first ) )
result << tmp;
}
}
QString nick = thisTree.getNickname();
if ( !nick.isEmpty() )
if ( nicknameToLob.contains( nick ) )
result << nick;
foreach ( const Lob& key, thisTree.attributeKeys() )
result << safeToAddNicknames( thisTree.attribute( key ) );
for ( unsigned int i = 0 ; i < thisTree.numChildren() ; i++ )
result << safeToAddNicknames( thisTree.child( i ) );
return result;
}
void LurchEnvironment::addToNicknameMap ( Lob thisTree )
{
QString nick = thisTree.getNickname();
if ( !nick.isEmpty() )
nicknameToLob[nick] = thisTree;
foreach ( const Lob& key, thisTree.attributeKeys() )
addToNicknameMap( thisTree.attribute( key ) );
for ( unsigned int i = 0 ; i < thisTree.numChildren() ; i++ )
addToNicknameMap( thisTree.child( i ) );
}
void LurchEnvironment::removeFromNicknameMap ( Lob thisTree )
{
QString nick = thisTree.getNickname();
if ( !nick.isEmpty() )
nicknameToLob.remove( nick );
foreach ( const Lob& key, thisTree.attributeKeys() )
removeFromNicknameMap( thisTree.attribute( key ) );
for ( unsigned int i = 0 ; i < thisTree.numChildren() ; i++ )
removeFromNicknameMap( thisTree.child( i ) );
}
Lob LurchEnvironment::lookupNickname ( QString nickname ) const
{
return nicknameToLob[nickname];
}
bool LurchEnvironment::load ( QString fnOrURN )
{
clearWarning();
// first try to load and see if it succeeds
QList<Lob> tmp;
QStringList tmp2;
QStringList tmp3;
if ( !recursiveLoad( fnOrURN, tmp, "", tmp2, tmp3 ) )
return error(); // propagate it up
if ( tmp.isEmpty() )
return error( "Loading " + fnOrURN + " produced no documents (quite unexpected)" );
// create nickname map, or give error if duplicates
QMap<QString,Lob> backup = nicknameToLob;
nicknameToLob.clear();
foreach ( const Lob& dep, tmp ) {
QStringList conflicts = safeToAddNicknames( dep );
if ( conflicts.count() == 0 ) {
addToNicknameMap( dep );
} else {
nicknameToLob = backup;
return error( "Duplicate nicknames were encountered at file "
+ ( ( dep == tmp.last() ) ? fnOrURN : dep.getURN() )
+ "; they were " + conflicts.join( ", " ) + "." );
}
}
// successful load, so remember which one is the document
int docIndex = tmp.count() - 1;
// and store all data
deps = tmp;
aliases = tmp3;
// append temporary dependencies (guaranteed OK by check in setTemporaryDependencies())
loadTemporaryDependencies();
// then pop the document out (done here in case one of the temporary dependencies, direct
// or indirect, was the current document itself)
doc = deps.takeAt( docIndex );
prepareNewDocument();
aliases.takeAt( docIndex );
// now instantiate package objects for all "loaded" packages
activePackages.clear();
for ( int i = 0 ; i < deps.count() ; i++ ) {
deps[i].setEditable( false );
activePackages << getPackage( deps[i].getURN() ); // will be NULL for non-packages
if ( activePackages.last() != NULL )
connect( activePackages.last(), SIGNAL(packageMessageToInterface(QString)),
this, SIGNAL(packageMessageToInterface(QString)) );
}
if ( documentPackage() != NULL ) {
doc = documentPackage()->asDocument( true ); // load visually presentable version
doc.setEditable( false );
}
// and do all processing of that loaded data
setDirty( false );
lastSaveFilename = Lob::isLurchURN( fnOrURN ) ? URNToFilename( fnOrURN ) : fnOrURN;
scriptUpdate();
// title was loaded from file, so not auto-generated
titleWasAutoGenerated = false;
return noErrors();
}
bool LurchEnvironment::recursiveLoad ( QString fnOrURN, QList<Lob>& results, QString alias,
QStringList& URNsDoneOrPending,
QStringList& URNaliases )
{
// first handle the case where fnOrURN is a valid Lurch URN
if ( Lob::isLurchURN( fnOrURN ) ) {
QString urn = Lob::basicLurchURN( fnOrURN );
if ( URNsDoneOrPending.contains( urn ) )
return noErrors(); // prevent circularity
// it's possible the URN refers to a package, and we try that possibility first:
LurchPackage* package = getPackage( urn );
if ( package != NULL ) {
URNsDoneOrPending << urn;
Lob asDoc = package->asDocument();
foreach ( const QString& dependency, asDoc.getDependencies() )
if ( !recursiveLoad( dependency, results, Lob::identifierOfURN( dependency ),
URNsDoneOrPending, URNaliases ) )
return error(); // propagate it up
results << asDoc;
URNaliases << alias;
return noErrors();
}
// the URN did not refer to a package, so we see if it refers to a file:
QString depfn = URNToFilename( urn );
if ( depfn.isEmpty() ) {
QStringList bits = Lob::splitLurchURN( urn );
return warning( QString( "Could not find dependency in search paths:\n"
"Title: %2\nAuthor: %3\nLanguage: %4\nVersion: %5\nURN: %1\n"
"Portions of the loaded document may not function." )
.arg( urn )
.arg( bits[0] ).arg( bits[1] ).arg( bits[2] ).arg( bits[3] ) );
}
// it did refer to a file, so we recur on that filename:
if ( !recursiveLoad( depfn, results, Lob::identifierOfURN( fnOrURN ),
URNsDoneOrPending, URNaliases ) )
return error(); // propagate it up
return noErrors();
}
// if we got here, then the first if clause in this routine failed, so fnOrURN is a filename
QFile f( fnOrURN );
QString maybeError;
Lob loadIntoThis = Lob::fromXML( f, &maybeError );
if ( maybeError.length() > 0 )
return error( maybeError );
if ( loadIntoThis.numChildren() < 1 )
return error( "The file " + f.fileName() + " contains no OpenMath objects" );
if ( loadIntoThis.numChildren() > 1 )
return error( "The file " + f.fileName() + " contains more than one OpenMath object" );
if ( !loadIntoThis.checkUniqueIDs() )
return error( "The file " + fnOrURN + " contains duplicate IDs." );
Lob loaded;
loaded.set( loadIntoThis.child() ); // smoothly deparent child from unknown-type wrapper
if ( !loaded.isDocument() )
return error( "The file " + f.fileName() + " does not contain a Lurch Document" );
URNsDoneOrPending << loaded.getURN();
foreach ( const QString& dependency, loaded.getDependencies() )
if ( !recursiveLoad( dependency, results, Lob::identifierOfURN( dependency ),
URNsDoneOrPending, URNaliases ) )
return error(); // propagate it up
results << loaded;
URNaliases << alias;
return noErrors();
}
QStringList LurchEnvironment::directToIndirectDependencies ( const QStringList& directDeps )
{
QList<Lob> loadedDocuments;
QStringList urns;
QStringList aliases;
foreach ( QString directDep, directDeps )
if ( !recursiveLoad( directDep, loadedDocuments, QString(), urns, aliases ) )
return QStringList(); // indicates error, which they can check with errorMessage()
QStringList results;
QMap<QString,QString> urn2alias;
for ( int i = 0 ; i < loadedDocuments.count() ; i++ )
if ( !aliases[i].isEmpty() )
urn2alias[loadedDocuments[i].getURN()] = aliases[i];
foreach ( QString urn, urns )
results << ( urn2alias.contains( urn ) ? ( urn2alias[urn] + "=" + urn ) : urn );
return results;
}
bool LurchEnvironment::reload ()
{
if ( isDirty() || lastSaveFilename.isEmpty() ) {
QString folder = temporaryFolder();
QTemporaryFile f( folder + "/temp_XXXXXX.lurch" );
if ( !f.open() )
return error( "Could not create a temporary file. "
"Try saving your changes then reloading." );
QString tempFilename = f.fileName();
QString oldLastSaveFilename = lastSaveFilename;
bool originalDirty = dirty;
bool wentOkay = save( tempFilename, false, false ) && load( tempFilename );
lastSaveFilename = oldLastSaveFilename; // otherwise it will be a funny tmp filename
setDirty( originalDirty );
return wentOkay ? noErrors() : error();
}
return load( lastSaveFilename );
}
bool LurchEnvironment::save ( QString filename, bool remember, bool makeChanges )
{
QFile f( filename );
QString maybeError;
if ( makeChanges ) {
doc.removeExtraIDs();
if ( titleWasAutoGenerated ) {
QStringList bits = Lob::splitLurchURN( doc.getURN() );
if ( bits.count() == 4 ) {
bits[0] = QFileInfo( filename ).baseName();
setCurrentDocumentURN( Lob::makeLurchURN( bits[0], bits[1], bits[2], bits[3] ) );
}
}
}
if ( !doc.save( f, &maybeError ) )
return error( maybeError );
if ( remember ) {
setDirty( false );
lastSaveFilename = filename;
}
return noErrors();
}
QString LurchEnvironment::filename() const
{
return lastSaveFilename;
}
bool LurchEnvironment::isDirty () const
{
return dirty;
}
void LurchEnvironment::setDirty ( bool on )
{
bool old = dirty;
dirty = on;
if ( old != dirty )
emit dirtyChanged( dirty );
}
void LurchEnvironment::newDocument ( QString urn )
{
scriptAction = LobChange();
deps.clear();
doc = Lob::newDocument( urn );
newInterpreter();
loadTemporaryDependencies();
prepareNewDocument();
ensureURNUniqueForAuthor();
setDirty( false );
lastSaveFilename = "";
}
bool LurchEnvironment::newDocumentFromTemplate ( QString filename, QString author )
{
if ( !load( filename ) ) {
QString keepError = errorMessage();
newDocument(); // this clears the error message, so we do the save thing above/below
return error( keepError );
}
scriptAction = LobChange();
ensureURNUniqueForAuthor( author );
setDirty( false );
lastSaveFilename = "";
return noErrors();
}
void LurchEnvironment::ensureURNUniqueForAuthor ( QString author )
{
QStringList urnbits = Lob::splitLurchURN( doc.getURN() );
if ( !author.isEmpty() )
urnbits[1] = author;
QString origTitle = urnbits[0];
int i = 0;
while ( !URNToFilename( Lob::makeLurchURN(
urnbits[0], urnbits[1], urnbits[2], urnbits[3] ) ).isEmpty() )
urnbits[0] = QString( "%1 #%2" ).arg( origTitle ).arg( ++i );
// first make sure we're not recording this change to later be undone/redone
watchType tmp = watchState;
watchState = UndoOrRedo; // a misnomer, but a functional one
// make changes
setCurrentDocumentURN( Lob::makeLurchURN( urnbits[0], urnbits[1],
urnbits[2], urnbits[3] ) );
titleWasAutoGenerated = true;
if ( doc.hasAttribute( "topic", "LurchCore" ) ) {
doc.removeAttribute( "topic", "LurchCore" );
doc.removeAttribute( "topic_description", "LurchCore" );
}
// return to old watch state
watchState = tmp;
}
bool LurchEnvironment::setTemporaryDependencies ( QStringList filenames )
{
tempdeps = filenames;
bool result = loadTemporaryDependencies();
if ( !result )
tempdeps.clear();
return result;
}
QStringList LurchEnvironment::getTemporaryDependencies () const
{
return tempdeps;
}
bool LurchEnvironment::loadTemporaryDependencies ()
{
// make a list of all currently loaded dependencies, to prevent re-loading/circularity
QStringList tmp;
QStringList tmp2;
foreach ( const Lob& dep, deps )
tmp << dep.getURN();
// load all temporary dependencies into newdeps
QList<Lob> newdeps;
foreach ( const QString& filename, tempdeps ) {
// if error occurs in loading temporary dependency, propagate it up
if ( !recursiveLoad( filename, newdeps, "", tmp, tmp2 ) ) {
numTempDepsLoaded = 0;
return error();
}
// but recursiveLoad() always loads the given file, even if it's already on the
// URNsDoneOrPending list, so see if we need to undo that, now knowing its URN:
for ( int i = 0 ; i < newdeps.count() - 1 ; i++ ) {
if ( newdeps[i].getURN() == newdeps.last().getURN() ) {
newdeps.takeLast();
tmp2.takeLast();
break;
}
}
}
// place all newdeps onto the list of actual deps
numTempDepsLoaded = newdeps.count();
for ( int i = 0 ; i < numTempDepsLoaded ; i++ ) {
deps << newdeps[i];
aliases << tmp2[i];
activePackages << NULL;
}
return noErrors();
}
int LurchEnvironment::numDependencies () const
{
return deps.count();
}
Lob LurchEnvironment::dependency ( int index ) const
{
if ( ( index < 0 ) || ( index >= deps.count() ) )
return Lob();
return deps[index];
}
bool LurchEnvironment::dependencyIsPackage ( int index ) const
{
if ( ( index < 0 ) || ( index >= deps.count() ) )
return false;
return activePackages[index] != NULL;
}
LurchPackage* LurchEnvironment::loadedPackage ( const char* className ) const
{
for ( int i = 0 ; i < deps.count() ; i++ )
if ( ( activePackages[i] != NULL ) && activePackages[i]->inherits( className ) )
return activePackages[i];
return NULL;
}
QString LurchEnvironment::alias ( int index ) const
{
if ( ( index < 0 ) || ( index >= aliases.count() ) )
return QString();
return aliases[index];
}
LobAddress LurchEnvironment::address ( const Lob docOrDepNode ) const
{
// find whether it's in a dependency or in the document
Lob firstAncestor = docOrDepNode;
while ( true )
if ( firstAncestor.hasParent() )
firstAncestor = firstAncestor.parent();
else if ( firstAncestor.hasContainer() )
firstAncestor = firstAncestor.container();
else
break;
int index = ( firstAncestor == doc ) ? deps.count() : deps.indexOf( firstAncestor );
// if it is neither, return an empty address to indicate failure
LobAddress tmp;
if ( index == -1 )
return tmp;
// otherwise, assemble the address with index as the first step, then the real address
tmp.addStep( index, OmNode::AsChild );
return tmp + docOrDepNode.address();
}
Lob LurchEnvironment::index ( const LobAddress address ) const
{
// if no first step, or first step indicates wrong type of address, return failure
if ( ( address.numSteps() == 0 ) || ( address.stepType( 0 ) != OmNode::AsChild ) )
return Lob();
// if first step is not into a dependency or the document, return failure
unsigned int index = address.stepIndex( 0 );
if ( index > ( unsigned int )( deps.count() ) )
return Lob();
// index into either the appropriate dependency or the document itself
Lob intoThis = ( index < ( unsigned int )( deps.count() ) ) ? deps[index] : doc;
return intoThis.index( address, 1 );
}
void LurchEnvironment::watchDocumentChanges ( const LobChange& change )
{
// if we're allowing changes, emit notification
if ( watchState == AllowChanges ) {
noteChange( change );
emit documentModified( change );
emit lateDocumentModified( change );
}
// If any changes were made to the document's URN or dependencies, undo them:
if ( ( watchState == PreventChanges )
&& ( change.context() == doc ) && ( change.address().numSteps() == 1 ) ) {
QString keyName;
QString keyCD;
if ( change.address().stepType( 0 ) == OmNode::AsValue ) {
keyName = change.keyName();
keyCD = change.keyCD();
if ( keyName.isEmpty() && keyCD.isEmpty() ) {
LobAddress addr;
addr.addStep( change.address().stepIndex( 0 ), OmNode::AsKey );
Lob key = change.context().index( addr );
keyName = key.toVariant().toStringList()[0];
keyCD = key.toVariant().toStringList()[1];
}
}
bool keyMatters = ( keyCD == "LurchCore" )
&& ( ( keyName == "author" ) || ( keyName == "title" )
|| ( keyName == "version" ) || ( keyName == "dependencies" ) );
if ( ( ( change.address().stepType( 0 ) == OmNode::AsValue )
&& ( keyCD == "LurchCore" ) && keyMatters ) // changed protected key
|| ( ( change.address().stepType( 0 ) == OmNode::AsChild )
&& ( change.address().stepIndex( 0 ) == 0 ) ) ) { // changed document symbol
error( "Cannot modify document URN or dependencies directly." );
watchState = InversionInProgress;
change.inverse().apply();
watchState = PreventChanges;
return;
}
}
// the lines immediately above are where the thing we're inverting stopped,
// so don't continue below with reversing stuff that never happened
if ( watchState == InversionInProgress )
return;
// if any old data went away...
if ( ( change.type() == LobChange::Remove ) || ( change.type() == LobChange::RemovePair )
|| ( change.type() == LobChange::Change ) ) {
// take its nicknames out of the lookup map
removeFromNicknameMap( change.oldData() );
}
// if any new data arrived...
if ( ( change.type() == LobChange::Insert ) || ( change.type() == LobChange::InsertPair )
|| ( change.type() == LobChange::Change ) ) {
// first, does this new data introduce any duplicate nicknames?
QStringList conflicts = safeToAddNicknames( change.newData() );
if ( conflicts.count() > 0 ) {
// if so, save an error message about them and prevent the change
error( "Cannot insert data into document; these nickname conflicts arise: "
+ conflicts.join( ", " ) + "." );
watchState = InversionInProgress;
change.inverse().apply();
watchState = PreventChanges;
return;
}
// otherwise we accept the change, so add its nicknames to our lookup table:
addToNicknameMap( change.result() );
}
// now that there's no more chance for this change to be revoked, update dirty flag:
setDirty( true );
// if any auto-run scripts changed, mark the script environment as dirty:
if ( !envDirty && ( change.oldData().containsScript( "auto-run" )
|| change.newData().containsScript( "auto-run" ) ) )
envDirty = true;
// if even a script attribute changed, mark the environment as dirty, because it may have
// been a script becoming auto-run, or un-becoming auto-run:
Lob modifiedLoc = change.result();
if ( !envDirty && modifiedLoc.hasContainer() && modifiedLoc.container().isScript() )
envDirty = true;
// note the change internally, then emit it to the outside world, and indicate success:
if ( watchState != UndoOrRedo )
noteChange( change );
emit documentModified( change );
emit lateDocumentModified( change );
noErrors();
}
bool LurchEnvironment::autoRunScripts ( const Lob doc, QString space )
{
if ( doc.isScript() && ( doc.scriptType() == "auto-run" ) ) {
interp->globalObject().setProperty( "__auto_run__",
interp->toScriptValue( doc ) );
QString code = doc.scriptCode();
FlexibleEvaluator::enterNamespace( space );
FlexibleEvaluator::enableCache();
QScriptValue result = FlexibleEvaluator::evaluate( code, interp );
interp->globalObject().setProperty( "__auto_run__", QScriptValue() );
if ( interp->hasUncaughtException() )
return error( QString( "Error: %1 in this code:\n%2" )
.arg( interp->uncaughtException().toString() )
.arg( getContext( code, interp->uncaughtExceptionLineNumber() ) ) );
} else {
foreach ( const Lob& key, doc.attributeKeys() )
if ( !autoRunScripts( doc.attribute( key ), space ) )
return error();
for ( unsigned int i = 0 ; i < doc.numChildren() ; i++ )
if ( !autoRunScripts( doc.child( i ), space ) )
return error();
}
return noErrors();
}
void LurchEnvironment::scriptUpdate ()
{
// if we're in the middle of a call to evaluate, doing this would actually be destructive
// and potentially cause a program crash, so just delay it until the current stack of
// nested evaluations completes, and it can happen before the next evaluate() starts
if ( interp->isEvaluating() )
return;
// We begin by marking the environment clean, even though we haven't yet updated it,
// because if some script below were to fail and abort early with an error, we still want
// to mark that we ran all the scripts that we *could*. Otherwise, what happens is that
// every time anything is to be evaluated, we will repeat the same evaluations of the same
// scripts in a new interpreter, bombing on the same error, for no good reason. This can
// actually cause highly unexpected behavior because every evaluation happens in a new
// interpreter, and thus script values expected to persist across function calls do not.
// So we mark the environment clean, and thus even if an error occurs, we won't try to
// rerun scripts to fix it unless of course a script changes, making the env dirty again.
envDirty = false;
newInterpreter();
LurchPackage* dp = documentPackage();
bool hadAutoRunScriptError = false;
for ( int i = 0 ; i < deps.count() ; i++ )
if ( dependencyIsPackage( i ) )
activePackages[i]->setEnvironment( this );
if ( dp != NULL )
dp->setEnvironment( this );
for ( int i = 0 ; i < deps.count() ; i++ ) {
if ( dependencyIsPackage( i ) ) {
if ( aliases[i].isEmpty() ) {
activePackages[i]->setup( interp->globalObject() );
if ( interp->hasUncaughtException() ) {
QStringList bits = Lob::splitLurchURN( activePackages[i]->getURN() );
emit autoRunScriptError( QString( "Error setting up package %1 by %2:\n"
"%3 in line %4" )
.arg( bits[0] ).arg( bits[1] )
.arg( interp->uncaughtException().toString() )
.arg( interp->uncaughtExceptionLineNumber() ) );
}
} else {
QScriptValue scope = interp->globalObject().property( aliases[i] );
if ( !scope.isValid() ) {
interp->globalObject().setProperty( aliases[i], interp->newObject() );
scope = interp->globalObject().property( aliases[i] );
QStringList bits = Lob::splitLurchURN( dependency( i ).getURN() );
scope.setProperty( "helpHTML",
QString( "A namespace created by the environment, "
"in which to put the data and functions for "
"the dependency \"%1\" by \"%2\"." )
.arg( bits[0] ).arg( bits[1] ) );
}
// let the script know the name of the scope it's being evaluated in
interp->globalObject().setProperty( "__dependency_alias__", aliases[i] );
activePackages[i]->setup( scope );
interp->globalObject().setProperty( "__dependency_alias__", QScriptValue() );
if ( interp->hasUncaughtException() ) {
QStringList bits = Lob::splitLurchURN( activePackages[i]->getURN() );
emit autoRunScriptError( QString( "Error setting up package %1 by %2:\n"
"%3 in line %4" )
.arg( bits[0] ).arg( bits[1] )
.arg( interp->uncaughtException().toString() )
.arg( interp->uncaughtExceptionLineNumber() ) );
}
}
}
if ( !hadAutoRunScriptError && !autoRunScripts( deps.at( i ), aliases[i] ) ) {
emit autoRunScriptError( "Error auto-running script in dependency "
+ deps.at( i ).getTitle() + ":\n" + errorMessage()
+ "\nAborting auto-running scripts, which may lead to "
"missing functionality and/or data." );
hadAutoRunScriptError = true;
}
// run the document's scripts before any temporary dependencies' scripts, because
// temporary dependencies are interpreted as being AFTER the document;
// this if clause happens just once, immediately after the last non-temp dependency
if ( i == deps.count() - 1 - numTempDepsLoaded ) {
if ( dp != NULL ) {
dp->setup( interp->globalObject() );
if ( interp->hasUncaughtException() ) {
QStringList bits = Lob::splitLurchURN( dp->getURN() );
emit autoRunScriptError( QString( "Error setting up package %1 by %2:\n"
"%3 in line %4" )
.arg( bits[0] ).arg( bits[1] )
.arg( interp->uncaughtException().toString() )
.arg( interp->uncaughtExceptionLineNumber() ) );
}
}
if ( !hadAutoRunScriptError && !autoRunScripts( doc ) ) {
emit autoRunScriptError( "Error auto-running script in the document:\n"
+ errorMessage()
+ "\nAborting auto-running scripts, which may lead "
"to missing functionality and/or data." );
hadAutoRunScriptError = true;
}
}
}
// if there were no dependencies at all, then the code in the loop above to auto-run
// scripts in the document will never be executed; handle that special case here:
if ( deps.count() == 0 ) {
if ( dp != NULL ) {
dp->setup( interp->globalObject() );
if ( interp->hasUncaughtException() ) {
QStringList bits = Lob::splitLurchURN( dp->getURN() );
emit autoRunScriptError( QString( "Error setting up package %1 by %2:\n"
"%3 in line %4" )
.arg( bits[0] ).arg( bits[1] )
.arg( interp->uncaughtException().toString() )
.arg( interp->uncaughtExceptionLineNumber() ) );
}
}
if ( !hadAutoRunScriptError && !autoRunScripts( doc ) ) {
emit autoRunScriptError( "Error auto-running script in the document:\n"
+ errorMessage()
+ "\nAborting auto-running scripts, which may lead "
"to missing functionality and/or data." );
hadAutoRunScriptError = true;
}
}
for ( int i = 0 ; i < deps.count() ; i++ )
if ( dependencyIsPackage( i ) )
activePackages[i]->initialize();
if ( dp != NULL )
dp->initialize();
}
void LurchEnvironment::noteChange ( LobChange initialChange )
{
scriptAction.recordStep( initialChange );
if ( ( scriptAction.type() != LobChange::NoChange )
&& ( scriptAction.type() != LobChange::Compound ) )
// then it's the first step of a change, so we must set up the timer
QTimer::singleShot( 0, this, SLOT(changesEnded()) );
}
void LurchEnvironment::changesEnded ()
{
if ( scriptAction.type() == LobChange::NoChange )
return;
if ( interp->isEvaluating() ) {
// the engine may continue to do more changes, so let's wait until it's really done
QTimer::singleShot( 0, this, SLOT(changesEnded()) );
return;
}
Q_ASSERT( ( scriptAction.type() != LobChange::Compound ) || ( scriptAction.count() > 0 ) );
actionDone( scriptAction, undoMerge );
redoStack.clear();
emit redoAvailableChanged( false );
undoMerge = false;
scriptAction = LobChange();
}
QScriptValue LurchEnvironment::evaluate ( QString script, QScriptValue activationObject,
bool withPrevious )
{
if ( envDirty )
scriptUpdate();
if ( activationObject.isValid() ) {
interp->pushContext();
copyProperties( activationObject, interp->currentContext()->activationObject() );
}
// we only use withPrevious if we haven't recorded any changes yet; if we have,
// this indicates that this call is coming in AMIDST what it considers the previous,
// which has not yet had its changesEnded() call take place, so the withPrevious
// is happening implicitly and shouldn't be doubled.
undoMerge = ( scriptAction.type() == LobChange::NoChange ) && withPrevious;
QScriptValue result = FlexibleEvaluator::evaluate( script, interp );
if ( activationObject.isValid() )
interp->popContext();
return result;
}
void LurchEnvironment::setCurrentDocumentURN ( QString urn )
{
if ( watchState == PreventChanges ) {
watchState = AllowChanges;
doc.setURN( urn );
watchState = PreventChanges;
} else {
doc.setURN( urn );
}
titleWasAutoGenerated = false;
}
bool LurchEnvironment::setCurrentDocumentDependencies ( QStringList dependencies )
{
// if there's no change to make, do nothing...
// unless this is the first setup of dependencies for this document
if ( ( dependencies == doc.getDependencies() ) && ( dependencies.count() > 0 ) )
return noErrors();
// make the change in the document
if ( watchState == PreventChanges ) {
watchState = AllowChanges;
doc.setDependencies( dependencies );
watchState = PreventChanges;
} else {
doc.setDependencies( dependencies );
}
// update aliases list from those new dependencies
aliases.clear();
for ( int i = 0 ; i < dependencies.count() ; i++ ) {
QStringList bits = Lob::splitLurchURN( dependencies[i] );
aliases << ( ( bits.count() == 5 ) ? bits[4] : QString() );
}
// reload from disk, to refresh all dependencies
return reload();
}
void LurchEnvironment::setDocumentMetadata ( Lob key, Lob value )
{
watchType oldState = watchState;
watchState = UndoOrRedo;
doc.setAttribute( key, value );
watchState = oldState;
}
QStringList LurchEnvironment::indexedURNs () const
{
return LurchPathTracker::indexedURNs() + LurchPackage::allURNs();
}
QString LurchEnvironment::errorMessage () const
{
return err;
}
bool LurchEnvironment::error ( QString message )
{
if ( message.length() )
err = message;
return false;
}
bool LurchEnvironment::noErrors ()
{
err = QString();
return true;
}
QString LurchEnvironment::warningMessage () const
{
return war;
}
bool LurchEnvironment::warning ( QString message )
{
if ( message.length() ) {
if ( war.length() )
war += "\n";
war += message;
}
return true;
}
void LurchEnvironment::clearWarning ()
{
war = QString();
}
QList<LurchEnvironment*> LurchEnvironment::environments;
int LurchEnvironment::index ()
{
return environments.indexOf( this );
}
LurchEnvironment* LurchEnvironment::lookupEnvironment ( int index )
{
return ( index < environments.count() ) ? environments[index] : NULL;
}
QScriptValue currentDocumentGetter ( QScriptContext* context, QScriptEngine* engine )
{
int index = (int)( context->callee().data().toInteger() );
LurchEnvironment* env = LurchEnvironment::lookupEnvironment( index );
// and env should be non-null, but the next line checks just to be sure
return engine->toScriptValue( env ? env->document() : Lob() );
}
QScriptValue numDependenciesGetter ( QScriptContext* context, QScriptEngine* engine )
{
int index = (int)( context->callee().data().toInteger() );
LurchEnvironment* env = LurchEnvironment::lookupEnvironment( index );
// and env should be non-null, but the next line checks just to be sure
return engine->toScriptValue( env ? (int)( env->numDependencies() ) : -1 );
}
QScriptValue dependencyGetter ( QScriptContext* context, QScriptEngine* engine )
{
int index = (int)( context->callee().data().toInteger() );
LurchEnvironment* env = LurchEnvironment::lookupEnvironment( index );
// and env should be non-null, but the next line checks just to be sure
if ( env == NULL )
return engine->toScriptValue( Lob() );
int depnum = (int)( context->argument( 0 ).toInteger() );
return engine->toScriptValue( env->dependency( depnum ) );
}
QScriptValue dependencyAliasGetter ( QScriptContext* context, QScriptEngine* engine )
{
int index = (int)( context->callee().data().toInteger() );
LurchEnvironment* env = LurchEnvironment::lookupEnvironment( index );
// and env should be non-null, but the next line checks just to be sure
if ( env == NULL )
return engine->toScriptValue( Lob() );
int depnum = (int)( context->argument( 0 ).toInteger() );
return engine->toScriptValue( env->alias( depnum ) );
}
QScriptValue nicknameLookup ( QScriptContext* context, QScriptEngine* engine )
{
int index = (int)( context->callee().data().toInteger() );
LurchEnvironment* env = LurchEnvironment::lookupEnvironment( index );
// and env should be non-null, but the next line checks just to be sure
if ( env == NULL )
return engine->toScriptValue( Lob() );
QString nickname;
if ( context->argument( 0 ).isString() ) {
nickname = context->argument( 0 ).toString();
} else {
Lob reference = engine->fromScriptValue<Lob>( context->argument( 0 ) );
if ( !reference.isNicknameReference() )
return engine->toScriptValue( Lob() );
nickname = reference.getNicknameReference();
}
return engine->toScriptValue( env->lookupNickname( nickname ) );
}
QScriptValue uiCall ( QScriptContext* context, QScriptEngine* engine )
{
int index = (int)( context->callee().data().toInteger() );
LurchEnvironment* env = LurchEnvironment::lookupEnvironment( index );
// and env should be non-null, but the next line checks just to be sure
if ( env == NULL )
return engine->toScriptValue( Lob() );
QScriptValue result = env->engine()->undefinedValue();
emit env->userInterfaceCall( context, engine, result );
return result;
}
void LurchEnvironment::newInterpreter ()
{
if ( interp != NULL ) {
if ( interp->isEvaluating() )
return;
delete interp;
}
interp = new QScriptEngine;
// general Lurch script setup
addLobsToScriptEngine( *interp );
// now add stuff specific to this class
QScriptValue docGetter = interp->newFunction( currentDocumentGetter );
docGetter.setData( QScriptValue( interp, index() ) );
docGetter.setProperty( "helpHTML",
"A function for fetching the currently loaded document, "
"as a Lob. (Takes no parameters.)<br>See also "
"<a href='execute help(numDependencies)'>numDependencies()</a> "
"and <a href='execute help(dependency)'>dependency()</a>." );
interp->globalObject().setProperty( "document", docGetter );
QScriptValue numDepGetter = interp->newFunction( numDependenciesGetter );
numDepGetter.setData( QScriptValue( interp, index() ) );
numDepGetter.setProperty( "helpHTML",
"A function for querying the number of dependencies of the "
"currently loaded document. (Takes no parameters.)<br>See also "
"<a href='execute help(document)'>document()</a> "
"and <a href='execute help(dependency)'>dependency()</a>." );
interp->globalObject().setProperty( "numDependencies", numDepGetter );
QScriptValue depGetter = interp->newFunction( dependencyGetter );
depGetter.setData( QScriptValue( interp, index() ) );
depGetter.setProperty( "helpHTML",
"A function for fetching a dependency of the given document "
"currently loaded document. Takes one parameter, the zero-based "
"integer index of the dependency to fetch.<br>See also "
"<a href='execute help(numDependencies)'>numDependencies()</a>, "
"<a href='execute help(alias)'>alias()</a>, "
"and <a href='execute help(document)'>document()</a>." );
interp->globalObject().setProperty( "dependency", depGetter );
QScriptValue aliasGetter = interp->newFunction( dependencyAliasGetter );
aliasGetter.setData( QScriptValue( interp, index() ) );
aliasGetter.setProperty( "helpHTML",
"A function for looking up the alias for the a dependency of the "
"currently loaded document. Takes one parameter, the zero-based "
"integer index of the dependency to fetch.<br>See also "
"<a href='execute help(numDependencies)'>numDependencies()</a> "
"and <a href='execute help(document)'>document()</a>." );
interp->globalObject().setProperty( "alias", aliasGetter );
QScriptValue lookuper = interp->newFunction( nicknameLookup );
lookuper.setData( QScriptValue( interp, index() ) );
lookuper.setProperty( "helpHTML",
"A function for looking up a Lob in the document or one of its "
"dependencies by nickname.<br><a href='DOX:class_lob.html#"
"69df86ed2d2a5c7572d9a52f0dee67de'>Read more online</a>" );
interp->globalObject().setProperty( "lookupNickname", lookuper );
QScriptValue uiCaller = interp->newFunction( uiCall );
uiCaller.setData( QScriptValue( interp, index() ) );
uiCaller.setProperty( "helpHTML",
"A function that makes it possible for the Lurch Environment to "
"communicate with the user interface. You should not need to "
"call this function from scripts." );
interp->globalObject().setProperty( "uiCommand", uiCaller );
emit newScriptEngine( interp );
}
bool LurchEnvironment::undoAvailable () const
{
return !undoStack.empty();
}
bool LurchEnvironment::redoAvailable () const
{
return !redoStack.empty();
}
void LurchEnvironment::setActionsInvisible ( bool on )
{
// if they're trying to turn it off, but we haven't yet recorded the change,
// don't let them; we'll turn it off for them in actionDone(), below.
if ( !on && ( scriptAction.type() != LobChange::NoChange ) )
return;
// otherwise, do just what they said
invisibleActions = on;
}
bool LurchEnvironment::getActionsInvisible () const
{
return invisibleActions;
}
void LurchEnvironment::setBusy ( bool on )
{
busy = on;
}
bool LurchEnvironment::isBusy () const
{
return busy;
}
void LurchEnvironment::actionDone ( const LobChange& change, bool withPrevious )
{
if ( ( invisibleActions || withPrevious ) && ( undoStack.count() > 0 ) )
undoStack.push( undoStack.pop().combine( change ) );
else
undoStack.push( change );
if ( undoStack.count() == 1 )
emit undoAvailableChanged( true );
invisibleActions = false;
}
void LurchEnvironment::actionUndone ( const LobChange& change )
{
redoStack.push( change );
if ( redoStack.count() == 1 )
emit redoAvailableChanged( true );
}
void LurchEnvironment::undo ()
{
if ( undoStack.empty() )
return;
LobChange toUndo = undoStack.pop();
if ( undoStack.empty() )
emit undoAvailableChanged( false );
watchState = UndoOrRedo;
toUndo.inverse().apply();
watchState = PreventChanges;
actionUndone( toUndo );
}
void LurchEnvironment::redo ()
{
if ( redoStack.empty() )
return;
LobChange toRedo = redoStack.pop();
if ( redoStack.empty() )
emit redoAvailableChanged( false );
watchState = UndoOrRedo;
toRedo.apply();
watchState = PreventChanges;
actionDone( toRedo );
}
LurchPackage* LurchEnvironment::getPackage ( QString urn )
{
if ( !urn2pkg.contains( urn ) )
urn2pkg[urn] = LurchPackage::create( urn );
return urn2pkg[urn];
}
LurchPackage* LurchEnvironment::documentPackage ()
{
return getPackage( doc.getURN() );
}