Transaction Manager and Inspector-style Command Object test This is a demonstration of a transitional modification to the panelset binding for bug 179621. The intention is to modify the existing Inspector command object system to include nsITransactionManager objects and JavaScript-based nsITransaction objects in a manner which does not interfere with the current command object structure. Section One: How Inspector's Command Object System Works Stack: --------------------------------... | | | | |* pointer |one |two |three |four |five Each new command object is added to the stack after the pointer, the stack gets truncated after the new command, and the pointer is advanced. This is by the panelset.execCommand() method. Undo operations move the pointer back and call undoCommand() on the last command. This is by the panelset.undo() method. Redo operations call redo on the currently-referenced command and move the pointer forward. This is by the panelset.redo() method. Sometimes a command object will tell the stack control routines not to add the command to the stack. For instance, if the command has been canceled or a dialog box must open for the command, the command will not be added to the stack immediately. In the case of a dialog box, the dialog may later add the command to the stack via the addCommandToStack() method. The structure of command objects is interesting, as they each have a unique constructor function. function cmdOne() { this.mButton = document.createElement("button"); this.mButton.setAttribute("label", "one"); } cmdOne.prototype = { doCommand: function() { test.appendChild(this.mButton); }, undoCommand: function() { test.removeChild(this.mButton); } }; A particular viewer object in DOM Inspector is responsible for generating these command objects via its getCommand() method, and has an isCommandEnabled() method to tell if that particular command is available. This is all workable (if a bit complex to figure out initially), if the command object system is indeed the only undo/redo control system working. Section Two: Multiple Transaction Manager Systems Unfortunately, the Editor project has its own Transaction Manager model to do essentially the same thing. Thus, for us to safely integrate Editor-style transactions (which we must do for editing text nodes, and for including DOM Inspector as a tab in the Editor application), some serious redesign is necessary. Ideally, a "virtual" transaction stack should work identically to the original, without distinguishing between the native Inspector model and the Transaction Manager model: Virtual Stack: ----------------------------------------------... | | | | |* pointer |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) The actual stack design would look something like this: Stack: ---------------------------------------... | | | |* pointer |one |TxMgr |four |TxMgr | | ---------- --- | |* pointer |* pointer |two |three |five This involves a serious redesign of the logic behind the primary stack, operated by Inspector. The remainder of this document will concentrate on the modifications needed to the primary stack and the panelset binding which owns it, followed by modifications to convert an Inspector-style command object into a transaction which can fit into an Editor-based Transaction Manager. Section Three: Redesigning the Primary Stack As far as the primary stack is concerned, an Editor-style transaction manager is just another command, but one that has its own undo and redo features. It's a stack within a stack, and so the primary stack has to learn how to deal with secondary stacks, and when to insert one. The ultimate goal is to have a virtual stack which acts just like a primary stack without secondary stacks would. If you want to skip all this theory, the following steps in the testcase should convince you that it works: cmdOne cmdTwoTxn cmdThreeTxn cmdFour cmdFiveTxn undo (5 times) redo (5 times) undo (2 times) cmdOne undo (2 times) cmdFiveTxn A few postulated rules: (1) Either a secondary stack or a command may be added to the primary stack. (2) If a command being added to the primary stack is incompatible with a secondary stack, add the command to the primary stack. (3) If a command being added to the primary stack is compatible with a secondary stack and the current command in the primary stack does not reference a secondary stack, add a secondary stack to the primary stack, and then add the command to the secondary stack. (4) If a command being added to the primary stack is compatible with a secondary stack and the current command in the primary stack references a secondary stack, add the command to the secondary stack. (5) An undo command on the primary stack should move the primary stack's pointer only if the next-undoable command on the primary stack is not a secondary stack or the secondary stack in question has at most one undoable command remaining. (6) An undo command should propagate to the preceding secondary stack on the primary stack only if there is at least one undoable command remaining on that stack and if the secondary stack is the command object preceding the current one on the primary stack. (7) A redo command on the primary stack should move the primary stack's pointer only if the current command on the primary stack is not a secondary stack or the secondary stack in question has no redoable commands remaining. (8) A redo command should propagate to the current secondary stack on the primary stack only if there is at least one redoable command remaining on that stack and if the secondary stack is the current command object on the primary stack. (9) A command added to the primary stack should eliminate all commands and secondary stacks following the inserted command on the primary stack. (10) A command added to a secondary stack should eliminate all commands following the inserted command on the secondary stack, and all commands and secondary stacks following the modified secondary stack on the primary stack. These rules define a structure for handling two levels of command stacks. To demonstrate, I'll explain a testcase you can implement in this document. First, let's define five commands: cmdOne, cmdTwoTxn, cmdThreeTxn, cmdFour, and cmdFive. cmdOne and cmdFour fit on our primary stack, but cmdTwoTxn, cmdThreeTxn, and cmdFiveTxn work only with secondary stacks (in our case, nsITransactionManager). Initially, our stack looks like this: Stack: --- * pointer == -1 Suppose we execute cmdOne on the primary stack. Stack: -----... |* pointer == 0 |cmdOne Postulate (1) allows us to do this, and Postulate (2) demands it. Nothing fancy. If we try an undo operation on this stack, Postulate (5) tells us the pointer doesn't reference a secondary stack, so we move the pointer while undoing the command. Stack: ------------------ * pointer == -1 | |cmdOne If we then try a redo operation on the stack, Postulate (6) tells us the pointer again doesn't reference a secondary stack, so we move the pointer while undoing the command. Stack: -----... |* pointer == 0 |cmdOne Next, we add cmdTwoTxn. Here, Postulate (3) determines the correct behavior. The pointer on the primary stack references the cmdOne object, so we must insert a secondary stack on the primary. Stack: ---------------------------------------... | |* pointer = 1; |one |TxMgr | --- |* pointer |two For now we'll skip the undo and redo operations (we'll see them later). Instead, we'll add cmdThreeTxn. Postulate (4) has its conditions met, so it applies: we add the new command directly to the secondary stack, and do nothing with the primary stack. Stack: ---------------------------------------... | |* pointer = 1; |one |TxMgr | --------- | |* pointer |two |three Next is cmdFour. Postulate (2) forces that one onto the primary stack. Stack: ---------------------------------------... | | |* pointer = 2; |one |TxMgr |four | --------- | |* pointer |two |three Then, for kicks, we'll try cmdFiveTxn, which incurs Postulate (3) again. Stack: ---------------------------------------... | | | |* pointer = 3; |one |TxMgr |four |TxMgr | | --------- ------ | |* pointer |* pointer |two |three |five For the moment, our "virtual" stack looks like this: Virtual Stack: ----------------------------------------------... | | | | |* pointer |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) Time for some undo fun. The "next-undoable" command on the primary stack is a secondary stack, but the secondary stack has exactly one undoable command. So, Postulate (6) propagates the undo operation down to the secondary stack. Stack: ---------------------------------------... | | | |* pointer = 3; |one |TxMgr |four |TxMgr | | --------- ------------ | |* pointer * pointer | |two |three |five By Postulate (5), the primary stack's pointer must move as well. Stack: ---------------------------------------... | | |* pointer = 2 | |one |TxMgr |four |TxMgr | | --------- ------------ | |* pointer * pointer | |two |three |five Virtual Stack: ------------------------------------------------... | | | |* pointer | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) So far, so good. The next undo operation should take us back a little bit further. Postulate (5) applies because the next-undoable command is not a secondary stack. So we move the primary stack pointer back one while undoing cmdFour. Stack: ---------------------------------------... | |* pointer = 1 | | |one |TxMgr |four |TxMgr | | --------- ------------ | |* pointer * pointer | |two |three |five Virtual Stack: ------------------------------------------------... | | |* pointer | | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) For the next undo operation, Postulate (6) applies, but Postulate (5) does not. This is because the secondary stack referenced by the primary stack has two commands on it. So the secondary pointer moves and the primary does not. Stack: ---------------------------------------... | |* pointer = 1 | | |one |TxMgr |four |TxMgr | | --------------- ------------ |* pointer | * pointer | |two |three |five Virtual Stack: ------------------------------------------------... | |* pointer | | | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) In the next undo operation, Postulates (5) and (6) apply, just like they did for the first undo operation. Stack: ------------------------------------------... |* pointer = 0 | | | |one |TxMgr |four |TxMgr | | ------------------ ------------ * pointer | | * pointer | |two |three |five Virtual Stack: --------------------------------------------------... |* pointer | | | | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) Finally, the last undo operation available only uses Postulate (5), just as the one for undoing the "four" command did. Stack: ---------------------------------------------... * pointer = -1 | | | | |one |TxMgr |four |TxMgr | | ------------------ ------------ * pointer | | * pointer | |two |three |five Virtual Stack: -----------------------------------------------------... * pointer | | | | | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) Up to this point, the virtual stack has correctly tracked the commands through initial action and through undo operations. Postulates (7) and (8) define redo operations, so we should probably go through the entire process (again)... Postulate (7) applies to the first redo operation because the next-redoable command on the primary stack is not a secondary stack. Stack: ------------------------------------------... |* pointer = 0 | | | |one |TxMgr |four |TxMgr | | ------------------ ------------ * pointer | | * pointer | |two |three |five Virtual Stack: --------------------------------------------------... |* pointer | | | | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) Postulate (7) applies to the next redo operation again because the current command isn't a secondary stack. Postulate (8) applies simultaneously to the next command (a secondary stack), because the secondary stack has a command to redo. Stack: ---------------------------------------... | |* pointer = 1 | | |one |TxMgr |four |TxMgr | | --------------- ------------ |* pointer | * pointer | |two |three |five Virtual Stack: ------------------------------------------------... | |* pointer | | | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) The next redo operation uses Postulate (8) but not Postulate (7). (The current command on the primary stack is a secondary stack with redoable operations.) Stack: ---------------------------------------... | |* pointer = 1 | | |one |TxMgr |four |TxMgr | | --------- ------------ | |* pointer * pointer | |two |three |five Virtual Stack: ------------------------------------------------... | | |* pointer | | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) The next redo operation uses Postulate (7) but not Postulate (8). There is nothing left to redo on the secondary stack. Stack: ---------------------------------------... | | |* pointer = 2 | |one |TxMgr |four |TxMgr | | --------- ------------ | |* pointer * pointer | |two |three |five Virtual Stack: ------------------------------------------------... | | | |* pointer | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) Finally, the last redo operation uses Postulate (7) and Postulate (8). Stack: ---------------------------------------... | | | |* pointer = 3; |one |TxMgr |four |TxMgr | | --------- ------ | |* pointer |* pointer |two |three |five For the moment, our "virtual" stack looks like this: Virtual Stack: ----------------------------------------------... | | | | |* pointer |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) Again, the virtual stack follows the rules of a traditional transaction manager. The only thing left to demonstrate is when a command is inserted into a stack and eliminates all commands after it in the undo/redo scheme. To demonstrate, we'll do two undo operations (without all the theory). Stack: ---------------------------------------... | |* pointer = 1 | | |one |TxMgr |four |TxMgr | | --------- ------------ | |* pointer * pointer | |two |three |five Virtual Stack: ------------------------------------------------... | | |* pointer | | |one |TxMgr(two) |TxMgr(three) |four |TxMgr(five) If I now execute the one command again, the four and five commands should disappear from all the stacks. Postulate (2) says the one command must appear on the primary stack. Postulate (9) forces the four command off the primary stack, and wipes the latter TxMgr secondary stack off the primary stack as well. Stack: -------------------------... | | |* pointer = 2 |one |TxMgr |one | --------- | |* pointer |two |three Virtual Stack: ----------------------------------------------... | | | |* pointer |one |TxMgr(two) |TxMgr(three) |one Two more undo operations, silently: Stack: -------------------------... | |* pointer = 1 | |one |TxMgr |one | --------------- |* pointer | |two |three Virtual Stack: -------------------------------------... | |* pointer | | |one |TxMgr(two) |TxMgr(three) |one And as a final demonstration, execute the five command again (which goes on a secondary stack, a Transaction Manager). Postulate (4) adds it to the secondary stack, and Postulate (10) erases the remainder of the secondary stack and the remainder of the primary stack. Stack: -------------------------... | |* pointer = 1 |one |TxMgr | --------- | |* pointer |two |five Virtual Stack: ----------------------... | | |* pointer |one |TxMgr(two) |TxMgr(five) Incidentally, you may have noticed something else, which I state as Theorem (11). (11) No two secondary stacks of the same type are ever adjacent to one another. I'll leave proving that up to you. Section Four: Converting a Command Constructor into a Transaction Constructor The nsITransaction IDL file defines several required methods (and one required property) of any transaction object for a TransactionManager (TxMgr) object to process. With help from Joaquin Blas (kin@netscape.com), I've figured out the bare minimum requirements (including a couple which weren't mentioned in the nsITransaction IDL file). The good news is that 90% of Inspector-based commands are directly translatable to TxMgr- compatible transactions. Not quite 100%, though... TxMgr Inspector ------------------------------------------ doTransaction() doCommand() undoTransaction() undoCommand() redoTransaction() doCommand() merge function () { return false; } isTransient doCommand() return value (most of the time) QueryInterface none available The doCommand() return value specifies in most instances whether a command should be on the stack or not. The exception is with dialog-based commands. These open dialog boxes, which then determine if a transaction actually happens. Based on this information, it shouldn't be too troublesome to create a function which converts a command constructor's prototype to an nsITransaction-compatible version. An actual command object in Inspector looks like this: function cmdOne() { this.mButton = document.createElement("button"); this.mButton.setAttribute("label", "one"); } cmdOne.prototype = { doCommand: function() { test.appendChild(this.mButton); }, undoCommand: function() { test.removeChild(this.mButton); } }; With a one-line addition, 90% of the time the command prototype could then reflect a transaction prototype: function cmdTwoTxn() { this.mButton = document.createElement("button"); this.mButton.setAttribute("label", "Two (txn)"); } cmdTwoTxn.prototype = { doCommand: function() { test.appendChild(this.mButton); }, undoCommand: function() { test.removeChild(this.mButton); } }; ConvertCommandToTxn(cmdTwoTxn); The ConvertCommandToTxn() function is a new function I propose for chrome://inspector/content/utils.js. // Functions for converting Inspector-style command constructors to nsITransaction constructors function ConvertCommandToTxn(constructor) { constructor.prototype.doTransaction = constructor.prototype.doCommand; constructor.prototype.undoTransaction = constructor.prototype.undoCommand; constructor.prototype.redoTransaction = constructor.prototype.doCommand; constructor.prototype.merge = ConvertCommandToTxn.txnMerge; constructor.prototype.QueryInterface = ConvertCommandToTxn.txnQueryInterface if (arguments.length > 1) { constructor.prototype.txnType = arguments[1]; } else { constructor.prototype.txnType = "standard"; } if (arguments.length > 2) { constructor.prototype.isTransient = arguments[2]; } else { constructor.prototype.isTransient = false; } } ConvertCommandToTxn.txnQueryInterface = function(theUID, theResult) { if (theUID == Components.interfaces.nsITransaction || theUID == Components.interfaces.nsISupports) { return this; } return null; } ConvertCommandToTxn.txnMerge = function() { return false; } You may note two properties of the transaction prototype it sets by default: isTransient and txnType. I've just explained isTransient; it's a Boolean value, either true or false. txnType is a special property for two reasons. In nsITransaction-compatible commands, it tells the primary stack methods of the panelset binding (chrome://inspector/content/inspector.xml ) whether the transaction is a standard one or one that opens a dialog box. The other reason is that Inspector-style command objects don't have a txnType property. The primary stack methods use the typeof operator to check whether command.txnType is undefined. This determines which mode these methods should operate in: primary-stack mode or secondary-stack mode. It's a nice little time-saver. So, the overall required properties and methods of a nsITransaction-compatible command constructor are: * doTransaction() (ConvertCommandToTxn, calls doCommand()) * undoTransaction() (ConvertCommandToTxn, calls undoCommand()) * redoTransaction() (ConvertCommandToTxn, calls doCommand()) * merge() (ConvertCommandToTxn, returns false) * isTransient (ConvertCommandToTxn, false default, true if not intended for stack) * QueryInterface() (ConvertCommandToTxn handles that) * txnType (ConvertCommandToTxn, "standard" default, "dialog" optional) * doCommand() (executes command, initally and in redo operations, in prototype of constructor) * undoCommand() (undoes command, in prototype of constructor) * openDialog() (opens dialog box, required only if txnType == "dialog", in prototype of constructor) All other properties of the command object should be defined in the command constructor function: function cmdTwoTxn() { this.mButton = document.createElement("button"); this.mButton.setAttribute("label", "Two (txn)"); } All other methods of the command object should be defined as properties of the command constructor's prototype object, before ConvertCommandToTxn() is called: cmdTwoTxn.prototype = { doCommand: function() { test.appendChild(this.mButton); }, undoCommand: function() { test.removeChild(this.mButton); } }; ConvertCommandToTxn(cmdTwoTxn); Dialog-based transactions should use: ConvertCommandToTxn(cmdName, "dialog"); Non-reversible transactions should use: ConvertCommandToTxn(cmdName, "standard", true); or: ConvertCommandToTxn(cmdName, "dialog", true); // if it's a dialog-based transaction. Section Five: Creating nsITransactionManager Objects Actually creating a nsITransactionManager object requires a little XPConnect, and for this I can only thank Joaquin Blas for the code I added to the Inspector panelset binding (with only the slightest of tweaks from me). Section Six: Transitional Status of the Suggested Patch Expect this documentation and the patch it accompanies to become obsolete by Mozilla 1.5 beta. By then, all commands in DOM Inspector should be converted to compatibility with nsITransaction, based on the documentation in Section Four of this document. At that point, there should be no need for the panelset's mCommandStack and mCommandPtr properties, and another major redesign to convert completely to an nsITransactionManager-based transaction control scheme. That being said, the patch this documentation relates to should be checked in as soon as it is viable. Though it does bloat the panelset binding a good deal, that is temporary and necessary to prevent regressions in DOM Inspector while converting commands to transactions. Fixing bug 179621 will require three, perhaps four phases. Phase One is this transitional patch phase: checking in a patch which allows for the multiple-stack model. Phase Two is the command conversion phase: transforming Inspector commands into nsITransaction objects. Note this is necessary while certain patches have new commands which haven't been implemented yet. Phase Three is the stack conversion phase: removing all traces of the original Inspector command stack scheme, to run a TxMgr-only model. Phase Four may or may not be necessary (but probably will be for bug 109682). It involves replacing the cmdEditUndo and cmdEditRedo Inspector commands with cmd_undo and cmd_redo, to use the proper command dispatcher and appropriate controllers.