@doc alias="recorder" hierarchy="GMLDOM">
The ModelRecorder is an object used for recording model changes (transactions)
and enabling undo/redo functionality
Instances of this class are created automatically by the system, and should
not be created directly. To access the ModelRecorder object of the currently
open model use the $ENV.recorder property.
(c) SAP AG 2003-2006. All rights reserved.
#INCLUDE[dev:defs.inc]
///////////////////////////////////////////////////////////////////////
// CLASS HEADER
Class ModelRecorder inherit Object;
<@doc hide="Y">
Constructs and initializes a new transactions ModelRecorder object
The model recorded by this object
constructor(model)
this.model = model;
this.past = [];
this.future = [];
this.present = null;
end
///////////////////////////////////////////////////////////////////////
// PROPERTIES
<@doc scope="private">Indicates whether the recorder is engaged in an undo or redo
readonly property engaged = false;
<@doc scope="private">Gets the model recorded by this object
readonly property model = ^gml:Model;
<@doc scope="private">Gets the past recordings buffer
readonly property past = null;
<@doc scope="private">Gets the current recording
readonly property present = null;
<@doc scope="private">Gets the future recordings buffer
readonly property future = null;
<@doc scope="private">Gets the recorder's transactions nesting level
readonly property level = 0;
<@doc scope="private">Gets a stamp value that can be used to monitor recorder state changes
readonly property stamp = 0;
/////////////////////////////////////////////////////////////////////
// METHODS
<@doc>
Indicates whether the recorder is at the beginning (i.e., there is nothing to undo)
Returns the test result
method bof()
return (this.past.length == 0);
end
<@doc>
Indicates whether the recorder is at the end (i.e., there is nothing to redo)
Returns the test result
method eof()
return (this.future.length == 0);
end
<@doc>
Executes a new transaction and appends it to the current recording.
The transaction type Id (see @env:KitTransactions!TRANSACTION_TYPE)
A variable-length arguments list to pass to the transaction constructor
The transaction object, or null in case of any error
If there is no current recording, a new recording containing the given transaction will be automatically
created.
method execute(type, p1, p2, p3, p4, p5, p6, p7)
try {
var tstor = $ENV.getTransType(type).trans;
var trans = new tstor(p1, p2, p3, p4, p5, p6, p7);
} catch(e) {
var msg1 = '#TEXT[XMSG_FAILED_TO_EXECUTE]';
var msg2 = 'transaction';
#LOG[4, msg1 + ' ' +type+ ' ' + msg2]
#TRACE[4, e.description]
return null;
}
if (!this.engaged) {
if (this.present) {
// append the transaction to the current recording
this.present.push(trans);
} else {
// automatically begin a new recording, append the transaction, and commit
this.begin(type);
this.present.push(trans);
this.commit();
}
}
return (trans.phase & #[TRANS_DO]) ? trans : null;
end
<@doc>
Begins a new recording
A display name to assign to the recording.
An object representing a snapshot of the current window at its last stable state prior to the recording (see @dev:Workspace!captureSnapshot). If omitted, the snapshot of the window at the exact moment that the recording has began will be used.
A transactions recording is a sequence of one or more transactions that are undone or redone in a single scope.
Once a recording has been started, you can use the ~add method to add new transactions to it.
As soon as you are done with the actions that constitute your scope you should call the ~commit resp. ~rollback method to accept resp. cancel the recording.
You must balance calls to the ~begin method with calls to the ~commit method. Nested calls to the ~begin method are merged, so at most a single recording is active at any time.
method begin(name, snapshot)
if (this.engaged) return;
if (!this.present) {
this.present = [];
this.present.name = name || '';
this.present.beforeshot = snapshot || $WIN.captureSnapshot();
}
this.level++;
end
<@doc>
Accepts the current recording
An object representing a snapshot of the current window at its next stable state after the recording. If omitted, the snapshot taken at the beginning of the recording will be used.
method commit(snapshot)
// validate present record
if (!this.present || this.engaged) return;
this.level--;
if (this.level > 0) return;
var record = this.present;
this.present = null;
this.level = 0;
if (record.length == 0) return;
record.aftershot = snapshot || record.beforeshot || null;
// push present record to the past, and clear the future
var len1=this.past.length, len2=this.future.length;
this.past.push(record);
if (len2 > 0) this.future.splice(0, len2);
if (len1 == 0 || len2 > 0) $WIN.adjustState(this.getState(), #[WIN_UNDO|WIN_REDO]);
this.stamp++;
end
<@doc>
Cancels the current recording
method rollback()
if (!this.present || this.engaged) return;
#LOG[1, "ROLLBACK was called, please view the trace for more details" ];
// undo the present record
var record = this.present;
var msg = [];
msg.push("ROLLBACK has been executed, The following entries have been rollbacked(in the order of execution):\n");
for (var i=record.length-1; i>=0; i--) {
try {
msg.push('Entry ' + i +' - ' + record[i].type+':\n'+this.entryToString(record[i]));
record[i].undo();
} catch (e) {
#TRACE[4, "Error in rollback: " + e.description];
}
}
#TRACE[1, JOIN(msg) ];
this.present = null;
this.level = 0;
// if nothing left to undo, clear dirty flag
if (this.past.length == 0) this.model.clearDirty();
// restore the window to the snapshot captured before the transaction was executed
if (record.beforeshot) $WIN.restoreSnapshot(record.beforeshot);
end
<@doc>
Reverses the last recording in the past, if any
method undo()
if (this.present || this.past.length == 0 || this.engaged) return;
this.engaged = true;
// peek record and test if context switch is needed
var record = this.past[this.past.length-1];
var snapshot = record.beforeshot;
var unit = $ENV.contextUnit;
if (snapshot && (!unit || unit.id != snapshot.unit_id)) {
$WIN.restoreSnapshot(snapshot);
this.engaged = false;
return;
}
// pop record from the past and push it to the future
var len1=this.past.length, len2=this.future.length;
this.future.push(this.past.pop());
if (len1 == 1 || len2 == 0) $WIN.adjustState(this.getState(), #[WIN_UNDO|WIN_REDO]);
// undo the popped record
for (var i=record.length-1; i>=0; i--) {
try {
record[i].undo();
} catch (e) {
#TRACE[4, "Error in undo: " + e.description];
}
}
// if nothing left to undo, clear dirty flag
if (this.past.length == 0) this.model.clearDirty();
// restore the window to the snapshot captured before the transaction was executed
if (snapshot) $WIN.restoreSnapshot(snapshot);
this.stamp++;
this.engaged = false;
end
<@doc>
Reapplies the first recording in the future, if any
method redo()
if (this.present || this.future.length == 0 || this.engaged) return;
this.engaged = true;
// peek record and test if context switch is needed
var record = this.future[this.future.length-1];
var snapshot = record.aftershot;
var unit = $ENV.contextUnit;
if (snapshot && (!unit || unit.id != snapshot.unit_id)) {
$WIN.restoreSnapshot(snapshot);
this.engaged = false;
return;
}
// pop record from the future and push it to the past
var len1=this.past.length, len2=this.future.length;
this.past.push(this.future.pop());
if (len1 == 0 || len2 == 1) $WIN.adjustState(this.getState(), #[WIN_UNDO|WIN_REDO]);
// redo the popped record
for (var i=0, len=record.length; i
Gets the recorder state
The recorder state
method getState()
return (this.past.length==0 ? 0 : #[WIN_UNDO]) + (this.future.length==0 ? 0 : #[WIN_REDO]);
end
<@doc">
Gets the recorder stamp
The recorder stamp
method getStamp()
return this.stamp;
end
<@doc>
Gets the recorder busy state.
~true if the recorder is in the middle of an undo, redo or rollback operation, ~false otherwise.
method getIsBusy()
return this.engaged;
end
<@doc scope="private">
Gets an entry as a string
A string representation of the entry
method entryToString(entry)
if (!entry) return null;
var str = [];
for (var key in entry) {
if (!entry.hasOwnProperty(key)) continue;
var value=entry[key];
switch (typeof value) {
case 'function': value='function'; break;
case 'string': value='"'+value+'"'; break;
}
str.push( '\t'+key+(key.length > 8 ? ':\t' : ':\t\t')+value+'\n');
}
return JOIN(str);
end