diff --git a/editor/svg-editor.js b/editor/svg-editor.js index 4d912722..284761da 100644 --- a/editor/svg-editor.js +++ b/editor/svg-editor.js @@ -32,7 +32,7 @@ function svg_edit_setup() { updateToolbar(); } // if (elem != null) - updateContextPanel(); + updateContextPanel(true); } // called when any element has changed @@ -42,7 +42,9 @@ function svg_edit_setup() { // positional/sizing information (we DON'T want to update the // toolbar here as that creates an infinite loop) if (elem == selectedElement) { - updateContextPanel(); + // we tell it to skip focusing the text control if the + // text element was previously in focus + updateContextPanel(false); } } @@ -76,7 +78,7 @@ function svg_edit_setup() { } // updates the context panel tools based on the selected element - function updateContextPanel() { + function updateContextPanel(shouldHighlightText) { var elem = selectedElement; $('#selected_panel').hide(); $('#rect_panel').hide(); @@ -123,7 +125,9 @@ function svg_edit_setup() { $('#font_size').val(elem.getAttribute("font-size")); $('#text').val(elem.textContent); $('#text').focus(); - $('#text').select(); + if (shouldHighlightText) { + $('#text').select(); + } break; } } @@ -320,6 +324,16 @@ function svg_edit_setup() { var clickSave = function(){ svgCanvas.save(); } + + var clickUndo = function(){ + if (svgCanvas.getUndoStackSize() > 0) + svgCanvas.undo(); + } + + var clickRedo = function(){ + if (svgCanvas.getRedoStackSize() > 0) + svgCanvas.redo(); + } $('#tool_select').click(clickSelect); $('#tool_path').click(clickPath); @@ -374,6 +388,9 @@ function svg_edit_setup() { $(document).bind('keydown', {combi:'down', disableInInput: true}, function(evt){moveSelected(0,1);evt.preventDefault();}); $(document).bind('keydown', {combi:'left', disableInInput: true}, function(evt){moveSelected(-1,0);evt.preventDefault();}); $(document).bind('keydown', {combi:'right', disableInInput: true}, function(evt){moveSelected(1,0);evt.preventDefault();}); + $(document).bind('keydown', {combi:'ctrl+z', disableInInput: true}, clickUndo); + $(document).bind('keydown', {combi:'ctrl+shift+z', disableInInput: true}, clickRedo); + $(document).bind('keydown', {combi:'ctrl+y', disableInInput: true}, clickRedo); var colorPicker = function(elem) { $('.tools_flyout').hide(); diff --git a/editor/svgcanvas.js b/editor/svgcanvas.js index 84a81e00..e71bb611 100644 --- a/editor/svgcanvas.js +++ b/editor/svgcanvas.js @@ -7,6 +7,90 @@ if(!window.console) { }; } +// These command objects are used for the Undo/Redo stack +// attrs contains the values that the attributes had before the change +function ChangeElementCommand(elem, attrs, text) { + this.elem = elem; + this.text = text ? ("Change " + elem.tagName + " " + text) : ("Change " + elem.tagName); + this.newValues = {}; + this.oldValues = attrs; + for (attr in attrs) { + if (attr == "#text") this.newValues[attr] = elem.textContent; + else this.newValues[attr] = elem.getAttribute(attr); + } + + this.apply = function() { + for( attr in this.newValues ) { + if (this.newValues[attr]) { + if (attr == "#text") this.elem.textContent = this.newValues[attr]; + else this.elem.setAttribute(attr, this.newValues[attr]); + } + else { + if (attr != "#text") this.elem.textContent = ""; + else this.elem.removeAttribute(attr); + } + } + return true; + }; + + this.unapply = function() { + for( attr in this.oldValues ) { + if (this.oldValues[attr]) { + if (attr == "#text") this.elem.textContent = this.oldValues[attr]; + else this.elem.setAttribute(attr, this.oldValues[attr]); + } + else { + if (attr == "#text") this.elem.textContent = ""; + else this.elem.removeAttribute(attr); + } + } + return true; + }; +} + +function InsertElementCommand(elem, text) { + this.elem = elem; + this.text = text || ("Create " + elem.tagName); + this.parent = elem.parentNode; + + this.apply = function() { this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling); }; + + this.unapply = function() { + this.parent = this.elem.parentNode; + this.elem = this.elem.parentNode.removeChild(this.elem); + }; +} + +function RemoveElementCommand(elem, parent, text) { + this.elem = elem; + this.text = text || ("Delete " + elem.tagName); + this.parent = parent; + + this.apply = function() { + this.parent = this.elem.parentNode; + this.elem = this.parent.removeChild(this.elem); + }; + + this.unapply = function() { this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling); }; +} + +function MoveElementCommand(elem, oldNextSibling, oldParent, text) { + this.elem = elem; + this.text = text ? ("Move " + elem.tagName + " to " + text) : ("Move " + elem.tagName + "top/bottom"); + this.oldNextSibling = oldNextSibling; + this.oldParent = oldParent; + this.newNextSibling = elem.nextSibling; + this.newParent = elem.parentNode; + + this.apply = function() { + this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling); + }; + + this.unapply = function() { + this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling); + }; +} + function SvgCanvas(c) { @@ -59,6 +143,26 @@ function SvgCanvas(c) var selectedOperation = 'resize'; // could be {resize,rotate} var events = {}; + var undoStackPointer = 0; + var undoStack = []; + + // FIXME: we MUST compress consecutive text changes to the same element + // (right now each keystroke is saved as a separate command that includes the + // entire text contents of the text element) + // TODO: consider limiting the history that we store here (need to do some slicing) + function addCommandToHistory(cmd) { + // if our stack pointer is not at the end, then we have to remove + // all commands after the pointer and insert the new command + if (undoStackPointer < undoStack.length && undoStack.length > 0) { + undoStack = undoStack.splice(0, undoStackPointer); + } + undoStack[undoStack.length] = cmd; + undoStackPointer = undoStack.length; +// console.log("after add command, stackPointer=" + undoStackPointer); +// console.log(undoStack); + } + + // private functions var getId = function() { if (events["getid"]) return call("getid",obj_num); @@ -142,16 +246,29 @@ function SvgCanvas(c) function recalculateSelectedDimensions() { var box = selected.getBBox(); + + // if we have not moved/resized, then immediately leave + if (box.x == selectedBBox.x && box.y == selectedBBox.y && + box.width == selectedBBox.width && box.height == selectedBBox.height) { + return; + } + + // after this point, we have some change + var remapx = function(x) {return ((x-box.x)/box.width)*selectedBBox.width + selectedBBox.x;} var remapy = function(y) {return ((y-box.y)/box.height)*selectedBBox.height + selectedBBox.y;} var scalew = function(w) {return w*selectedBBox.width/box.width;} var scaleh = function(h) {return h*selectedBBox.height/box.height;} + + var changes = {}; selected.removeAttribute("transform"); switch (selected.tagName) { case "path": // extract the x,y from the path, adjust it and write back the new path + // but first, save the old path + changes["d"] = selected.getAttribute("d"); var M = selected.pathSegList.getItem(0); var curx = M.x, cury = M.y; var newd = "M" + remapx(curx) + "," + remapy(cury); @@ -173,18 +290,29 @@ function SvgCanvas(c) selected.setAttributeNS(null, "d", newd); break; case "line": + changes["x1"] = selected.x1.baseVal.value; + changes["y1"] = selected.y1.baseVal.value; + changes["x2"] = selected.x2.baseVal.value; + changes["y2"] = selected.y2.baseVal.value; selected.x1.baseVal.value = remapx(selected.x1.baseVal.value); selected.y1.baseVal.value = remapy(selected.y1.baseVal.value); selected.x2.baseVal.value = remapx(selected.x2.baseVal.value); selected.y2.baseVal.value = remapy(selected.y2.baseVal.value); break; case "circle": + changes["cx"] = selected.cx.baseVal.value; + changes["cy"] = selected.cy.baseVal.value; + changes["r"] = selected.r.baseVal.value; selected.cx.baseVal.value = remapx(selected.cx.baseVal.value); selected.cy.baseVal.value = remapy(selected.cy.baseVal.value); // take the minimum of the new selected box's dimensions for the new circle radius selected.r.baseVal.value = Math.min(selectedBBox.width/2,selectedBBox.height/2); break; case "ellipse": + changes["cx"] = selected.cx.baseVal.value; + changes["cy"] = selected.cy.baseVal.value; + changes["rx"] = selected.rx.baseVal.value; + changes["ry"] = selected.ry.baseVal.value; selected.cx.baseVal.value = remapx(selected.cx.baseVal.value); selected.cy.baseVal.value = remapy(selected.cy.baseVal.value); selected.rx.baseVal.value = scalew(selected.rx.baseVal.value); @@ -192,10 +320,16 @@ function SvgCanvas(c) break; case "text": // cannot use x.baseVal.value here because x is a SVGLengthList + changes["x"] = selected.getAttribute("x"); + changes["y"] = selected.getAttribute("y"); selected.setAttribute("x", remapx(selected.getAttribute("x"))); selected.setAttribute("y", remapy(selected.getAttribute("y"))); break; case "rect": + changes["x"] = selected.x.baseVal.value; + changes["y"] = selected.y.baseVal.value; + changes["width"] = selected.width.baseVal.value; + changes["height"] = selected.height.baseVal.value; selected.x.baseVal.value = remapx(selected.x.baseVal.value); selected.y.baseVal.value = remapy(selected.y.baseVal.value); selected.width.baseVal.value = scalew(selected.width.baseVal.value); @@ -206,6 +340,10 @@ function SvgCanvas(c) break; } // fire changed event + if (changes) { + var text = (current_resize_mode == "none" ? "position" : "size"); + addCommandToHistory(new ChangeElementCommand(selected, changes, text)); + } call("changed", selected); } @@ -346,6 +484,11 @@ function SvgCanvas(c) call("selected", selected); } + // in mouseDown : + // - when we are in a create mode, the element is added to the canvas + // but the action is not recorded until mouseUp + // - when we are in select mode, select the element, remember the position + // and do nothing else var mouseDown = function(evt) { var x = evt.pageX - container.offsetLeft; @@ -355,6 +498,7 @@ function SvgCanvas(c) started = true; start_x = x; start_y = y; + current_resize_mode = "none"; var t = evt.target; // WebKit returns