diff --git a/app/app.xhtml b/app/app.xhtml index 0f8d5221..5cf29cff 100644 --- a/app/app.xhtml +++ b/app/app.xhtml @@ -211,6 +211,8 @@ + + diff --git a/app/index.js b/app/index.js index ecbca651..a1f5539f 100644 --- a/app/index.js +++ b/app/index.js @@ -96,11 +96,13 @@ app.on("window-all-closed", function() { app.on('ready', function() { protocol.registerBufferProtocol("ref", function(request, callback) { var path = request.url.substr(6); - console.log("PATH", path); fs.readFile(path, function (err, data) { - console.log("Got data: ", data.length); - callback({mimeType: "image/jpeg", data: new Buffer(data)}); + if (err) { + callback({mimeType: "text/html", data: new Buffer("Not found")}); + } else { + callback({mimeType: "image/jpeg", data: new Buffer(data)}); + } }); }, function (error, scheme) { diff --git a/app/package.json b/app/package.json index c6d87426..9b1e14ca 100644 --- a/app/package.json +++ b/app/package.json @@ -2,7 +2,7 @@ "name": "Pencil", "productName": "Pencil", "description": "An open-source GUI prototyping tool that is available for ALL platforms.", - "version": "3.0.1", + "version": "3.0.2", "author": { "name": "Evolus", "url": "http://evolus.vn", @@ -43,20 +43,22 @@ "adm-zip": "^0.4.7", "archive-type": "^3.2.0", "archiver": "^1.3.0", + "decompress": "^4.0.0", + "decompress-targz": "^4.0.0", "easy-zip2": "^1.0.0", "electron-log": "^1.3.0", "electron-updater": "^1.4.1", "less": "~2.7.1", "lodash": "^4.13.1", + "md5": "^2.2.1", "moment": "^2.13.0", "nugget": "^2.0.0", "q": "^1.4.1", "rimraf": "^2.5.4", + "tar": "https://github.com/dgthanhan/node-tar/archive/6086b1ea82137c61eea4efe882dd514590e5b7a8.tar.gz", "tar.gz": "^1.0.5", "tmp": "0.0.31", - "unzip2": "^0.2.5", - "decompress": "^4.0.0", - "decompress-targz": "^4.0.0" + "unzip2": "^0.2.5" }, "private": true } diff --git a/app/pencil-core/behavior/SVGTextLayout.js b/app/pencil-core/behavior/SVGTextLayout.js index 92445670..eac3c872 100644 --- a/app/pencil-core/behavior/SVGTextLayout.js +++ b/app/pencil-core/behavior/SVGTextLayout.js @@ -260,10 +260,13 @@ SVGHTMLRenderer.HANDLERS = { } }; +SVGHTMLRenderer.STYLE_NAME_MAP = { + fill: "color" +} SVGHTMLRenderer.prototype.importDefaultStyleFromNode = function (node) { for (var name in this.defaultStyle) { var value = node.style[name]; - if (value) this.defaultStyle[name] = value; + if (value) this.defaultStyle[SVGHTMLRenderer.STYLE_NAME_MAP[name] || name] = value; } }; SVGHTMLRenderer.prototype.renderHTML = function (html, container, view) { @@ -272,6 +275,7 @@ SVGHTMLRenderer.prototype.renderHTML = function (html, container, view) { var div = doc.createElementNS(PencilNamespaces.html, "div"); div.style.position = "absolute"; div.style.display = "none"; + div.style.textAlign = ["left", "center", "right"][this.hAlign || 0]; doc.body.appendChild(div); for (var styleName in this.defaultStyle) { @@ -296,7 +300,7 @@ SVGHTMLRenderer.prototype.render = function (nodes, container, view) { var target = container; if (vAlign > 0) { var last = layouts[layouts.length - 1]; - var height = last.y + last.height; + var height = last.y + last.height - (view.y || 0); dy = Math.round((this.height - height) * vAlign / 2); var target = container.ownerDocument.createElementNS(PencilNamespaces.svg, "g"); target.setAttribute("transform", "translate(0," + dy + ")"); diff --git a/app/pencil-core/behavior/commonBehaviors.js b/app/pencil-core/behavior/commonBehaviors.js index 36dc7c5c..30d29957 100644 --- a/app/pencil-core/behavior/commonBehaviors.js +++ b/app/pencil-core/behavior/commonBehaviors.js @@ -37,8 +37,7 @@ Pencil.behaviors.Color = function (color) { Svg.setStyle(this, "fill", color.toRGBString()); Svg.setStyle(this, "fill-opacity", color.a); } else { - Svg.setStyle(this, "color", color ? color.toRGBString() : null); - Svg.setStyle(this, "opacity", color ? color.a : null); + Svg.setStyle(this, "color", color ? color.toRGBAString() : null); } }; Pencil.behaviors.StrokeColor = function (color) { @@ -646,8 +645,25 @@ Pencil.behaviors.NPatchDomContent = function (nPatch, dim) { Pencil.behaviors.NPatchDomContentFromImage = function (imageData, dim, xAnchorMaps, yAnchorMaps) { //sorting var xCells = imageData.xCells; - if (!xCells || xCells.length == 0) xCells = [{from: 0, to: imageData.w}]; var yCells = imageData.yCells; + + if ((!xCells || xCells.length == 0) && (!yCells || yCells.length == 0)) { + Dom.empty(this); + + this.setAttribute("width", dim.w) + this.setAttribute("height", dim.h) + this.setAttribute("style", "line-height: 1px;"); + + this.appendChild(Dom.newDOMElement({ + _name: "img", + _uri: PencilNamespaces.html, + style: new CSS().set("width", dim.w + "px").set("height", dim.h + "px").toString(), + src: ImageData.refStringToUrl(imageData.data) || imageData.data + })); + return; + } + + if (!xCells || xCells.length == 0) xCells = [{from: 0, to: imageData.w}]; if (!yCells || yCells.length == 0) yCells = [{from: 0, to: imageData.h}]; xCells = [].concat(xCells); diff --git a/app/pencil-core/canvasHelper/canvasImpl.js b/app/pencil-core/canvasHelper/canvasImpl.js index 1d01b4b5..4086d3ee 100644 --- a/app/pencil-core/canvasHelper/canvasImpl.js +++ b/app/pencil-core/canvasHelper/canvasImpl.js @@ -1,6 +1,8 @@ var CanvasImpl = {}; CanvasImpl.setupGrid = function () { + CanvasImpl.drawMargin.call(this); + if (this.gridContainer) { Dom.empty(this.gridContainer); } else { @@ -42,3 +44,37 @@ CanvasImpl.setupGrid = function () { } } }; +CanvasImpl.drawMargin = function () { + var unzommedMargin = Pencil.controller.getDocumentPageMargin(); + if (!unzommedMargin) { + if (this.marginPath) this.marginPath.parentNode.removeChild(this.marginPath); + this.marginPath = null; + return; + } + + var margin = unzommedMargin * this.zoom; + var color = Config.get(Config.DEV_PAGE_MARGIN_COLOR); + + if (!this.marginPatternDef) { + // this.marginPatternDef = Dom.newDOMElement({ + // + // }); + // this.bgLayer.appendChild(this.marginPatternDef); + } + + if (!this.marginPath) { + this.marginPath = document.createElementNS(PencilNamespaces.svg, "svg:path"); + this.marginPath.setAttributeNS(PencilNamespaces.p, "p:name", "margins"); + this.marginPath.setAttribute("stroke", "none"); + this.marginPath.setAttribute("fill", color); + this.bgLayer.appendChild(this.marginPath); + } + + var x = 5; + var width = this.width * this.zoom; + var height = this.height * this.zoom; + this.marginPath.setAttribute("d", [ + M(0 - x, 0 - x), L(0 - x, height + x), L(width + x, height + x), L(width + x, 0 - x), z, + M(margin, margin), L(width - margin, margin), L(width - margin, height - margin), L(margin, height - margin), z + ].join(" ")); +}; diff --git a/app/pencil-core/canvasHelper/snappingHelper.js b/app/pencil-core/canvasHelper/snappingHelper.js index 6fe06d57..a943bd27 100644 --- a/app/pencil-core/canvasHelper/snappingHelper.js +++ b/app/pencil-core/canvasHelper/snappingHelper.js @@ -111,6 +111,23 @@ SnappingHelper.prototype.rebuildSnappingGuide = function () { } } } + + var margin = Pencil.controller.getDocumentPageMargin(); + if (margin) { + var uid = Util.newUUID(); + this.snappingGuide[uid] = { + vertical: [ + new SnappingData("MarginSnap", margin, "Left", true, uid), + new SnappingData("MarginSnap", this.canvas.width - margin, "Right", true, uid) + ], + horizontal: [ + new SnappingData("MarginSnap", margin, "Top", false, uid), + new SnappingData("MarginSnap", this.canvas.height - margin, "Bottom", false, uid) + ] + }; + + } + this.sortData(); }; SnappingHelper.prototype.updateSnappingGuide = function (controller, remove) { diff --git a/app/pencil-core/common/Canvas.js b/app/pencil-core/common/Canvas.js index fa560627..03a7527c 100644 --- a/app/pencil-core/common/Canvas.js +++ b/app/pencil-core/common/Canvas.js @@ -178,6 +178,7 @@ function Canvas(element) { thiz.handleClick(event); }, false); this.svg.addEventListener("mousedown", function (event) { + thiz.movementDisabled = Pencil.controller.movementDisabled || event.ctrlKey; // document.commandDispatcher.advanceFocus(); thiz.focus(); thiz.handleMouseDown(event); @@ -338,6 +339,10 @@ function Canvas(element) { CanvasImpl.setupGrid.apply(this); } }.bind(this)); + window.globalEventBus.listen("doc-options-change", function (data) { + CanvasImpl.drawMargin.apply(this); + this.snappingHelper.rebuildSnappingGuide(); + }.bind(this)); } @@ -1148,6 +1153,8 @@ Canvas.prototype.handleMouseMove = function (event, fake) { if (!fake) { event.preventDefault(); event.stopPropagation(); + + if (this.movementDisabled) return; } if (this.currentController.markAsMoving) @@ -1386,7 +1393,7 @@ Canvas.prototype.handleKeyPress = function (event) { event.preventDefault(); } } else if (event.keyCode == DOM_VK_F2) { - if (this.currentController) { + if (this.currentController && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) { Dom.emitEvent("p:TextEditingRequested", this.element, { controller : this.currentController }); @@ -2520,21 +2527,42 @@ Canvas.prototype.sizeToContent = function (hPadding, vPadding) { return newSize; }; Canvas.prototype.sizeToContent__ = function (hPadding, vPadding) { + var pageMargin = Pencil.controller.getDocumentPageMargin() || 0; + + hPadding += pageMargin; + vPadding += pageMargin; this.zoomTo(1.0); var thiz = this; var maxBox = null; + function out(name, rect) { + console.log(name, "left: ", rect.left, "top: ", rect.top, "width: ", rect.width, "height: ", rect.height); + } + out("this.svg", this.svg.getBoundingClientRect()); Dom.workOn("./svg:g[@p:type]", this.drawingLayer, function (node) { try { var controller = thiz.createControllerFor(node); + if (controller.def && controller.def.meta.excludeSizeCalculation) return; var bbox = controller.getBoundingRect(); + //HACK: inspect the strokeWidth attribute to fix half stroke bbox issue var box = { x1 : bbox.x, y1 : bbox.y, x2 : bbox.x + bbox.width, y2 : bbox.y + bbox.height }; + + if (controller.getGeometry) { + var geo = controller.getGeometry(); + if (geo.ctm && geo.ctm.a == 1 && geo.ctm.b == 0 && geo.ctm.c == 0 && geo.ctm.d == 1 && geo.dim) { + box.x1 = geo.ctm.e; + box.y1 = geo.ctm.f; + box.x2 = box.x1 + geo.dim.w; + box.y2 = box.y1 + geo.dim.h; + } + } + if (maxBox == null) { maxBox = box; } else { @@ -2556,6 +2584,10 @@ Canvas.prototype.sizeToContent__ = function (hPadding, vPadding) { Util.getMessage("button.cancel.close")); return; } + maxBox.x1 = Math.floor(maxBox.x1); + maxBox.x2 = Math.ceil(maxBox.x2); + maxBox.y1 = Math.floor(maxBox.y1); + maxBox.y2 = Math.ceil(maxBox.y2); var width = maxBox.x2 - maxBox.x1 + 2 * hPadding; var height = maxBox.y2 - maxBox.y1 + 2 * vPadding; diff --git a/app/pencil-core/common/EpgzHandler.js b/app/pencil-core/common/EpgzHandler.js index a42e4717..f454bb05 100644 --- a/app/pencil-core/common/EpgzHandler.js +++ b/app/pencil-core/common/EpgzHandler.js @@ -28,6 +28,7 @@ EpgzHandler.prototype.loadDocument = function(filePath) { EpgzHandler.prototype.saveDocument = function (documentPath) { return new Promise(function (resolve, reject) { + var path = null; var targz = require('tar.gz'); new targz({}, {fromBase: true}).compress(Pencil.documentHandler.tempDir.name, documentPath) .then(resolve).catch(reject); diff --git a/app/pencil-core/common/config.js b/app/pencil-core/common/config.js index d01d2f72..b764dd40 100644 --- a/app/pencil-core/common/config.js +++ b/app/pencil-core/common/config.js @@ -51,3 +51,19 @@ try { } Config._load(); + + + +//Specific configuration schema management +Config.define = function (name, defaultValue) { + if (Config.get(name, null) === null) { + Config.set(name, defaultValue); + } + + return name; +}; + +Config.DEV_PAGE_MARGIN_SIZE = Config.define("dev.pageMargin.size", Config.DEV_DEFAULT_PAGE_MARGIN); +Config.DEV_PAGE_MARGIN_COLOR = Config.define("dev.pageMargin.color", "rgba(0, 0, 0, 0.2)"); + +Config.DEV_ENABLE_DISABLED_IN_PROP_PAGE = Config.define("dev.enable_disabled_in_property_page", false); diff --git a/app/pencil-core/common/controller.js b/app/pencil-core/common/controller.js index 83eecae1..0a56eb6f 100644 --- a/app/pencil-core/common/controller.js +++ b/app/pencil-core/common/controller.js @@ -28,7 +28,7 @@ Controller.prototype.makeSubDir = function (sub) { return fullPath; }; Controller.prototype.getDocumentName = function () { - return this.documentPath ? path.basename(this.documentPath).replace(/\.epz$/, "") : "* Unsaved document"; + return this.documentPath ? path.basename(this.documentPath).replace(/\.[a-z]+$/, "") : "* Unsaved document"; }; // Controller.prototype.newDocument = function () { // var thiz = this; @@ -1057,6 +1057,15 @@ Controller.prototype.copyAsRef = function (sourcePath, callback) { rd.pipe(wr); }; +Controller.prototype.generateCollectionResourceRefId = function (collection, resourcePath) { + var id = "collection " + collection.id + " " + resourcePath; + var md5 = require("md5"); + id = md5(id) + path.extname(resourcePath); + + id = id.replace(/[^a-z\-0-9]+/gi, "_"); + + return id; +}; Controller.prototype.collectionResourceAsRefSync = function (collection, resourcePath) { var parts = resourcePath.split("/"); sourcePath = collection.installDirPath; @@ -1069,8 +1078,7 @@ Controller.prototype.collectionResourceAsRefSync = function (collection, resourc return null; } - var id = "collection " + collection.id + " " + resourcePath; - id = id.replace(/[^a-z\-0-9]+/gi, "_"); + var id = this.generateCollectionResourceRefId(collection, resourcePath); var filePath = path.join(this.makeSubDir(Controller.SUB_REFERENCE), id); @@ -1414,6 +1422,18 @@ Controller.prototype.exportAsLayout = function () { }); }; +Controller.prototype.getDocumentPageMargin = function () { + if (!StencilCollectionBuilder.isDocumentConfiguredAsStencilCollection()) { + return null; + } + var options = StencilCollectionBuilder.getCurrentDocumentOptions(); + if (!options) { + return null; + } + + return options.pageMargin || Config.get(Config.DEV_PAGE_MARGIN_SIZE) || 40; +}; + window.onbeforeunload = function (event) { // Due to a change of Chrome 51, returning non-empty strings or true in beforeunload handler now prevents the page to unload diff --git a/app/pencil-core/common/renderer.js b/app/pencil-core/common/renderer.js index 58e09b8b..a1c1158c 100644 --- a/app/pencil-core/common/renderer.js +++ b/app/pencil-core/common/renderer.js @@ -132,28 +132,42 @@ module.exports = function () { }; } + var initialized = false; + function init() { + if (rendererWindow) { + try { + rendererWindow.destroy(); + } catch (e) {} + } + rendererWindow = new BrowserWindow({x: 0, y: 0, useContentSize: true, enableLargerThanScreen: true, show: false, frame: false, autoHideMenuBar: true, transparent: true, webPreferences: {webSecurity: false, defaultEncoding: "UTF-8"}}); // rendererWindow.webContents.openDevTools(); - ipcMain.on("render-request", function (event, data) { - queueHandler.submit(createRenderTask(event, data)); - }); + queueHandler.tasks = []; - ipcMain.on("render-rendered", function (event, data) { - setTimeout(function () { - if (currentRenderHandler) currentRenderHandler(event, data); - }, 100); - }); + if (!initialized) { + ipcMain.on("render-request", function (event, data) { + queueHandler.submit(createRenderTask(event, data)); + }); - ipcMain.on("font-loading-request", function (event, data) { - fontFaceCSS = sharedUtil.buildFontFaceCSS(data.faces); - event.sender.send(data.id, {}); - }); + ipcMain.on("render-rendered", function (event, data) { + setTimeout(function () { + if (currentRenderHandler) currentRenderHandler(event, data); + }, 100); + }); + ipcMain.on("font-loading-request", function (event, data) { + fontFaceCSS = sharedUtil.buildFontFaceCSS(data.faces); + event.sender.send(data.id, {}); + }); + console.log("RENDERER started."); + } else { + console.log("RENDERER re-started."); + } - console.log("RENDERER started."); + initialized = true; } function initOutProcessCanvasBasedRenderer() { var canvasWindow = new BrowserWindow({x: 0, y: 0, enableLargerThanScreen: true, show: false, autoHideMenuBar: true, webPreferences: {webSecurity: false, defaultEncoding: "UTF-8"}}); @@ -195,6 +209,9 @@ module.exports = function () { ipcMain.once("render-init", function (event, data) { init(); }); + ipcMain.on("render-restart", function (event, data) { + init(); + }); ipcMain.once("canvas-render-init", function (event, data) { initOutProcessCanvasBasedRenderer(); }); diff --git a/app/pencil-core/common/svgRasterizer.js b/app/pencil-core/common/svgRasterizer.js index 38f22612..48b4eba9 100644 --- a/app/pencil-core/common/svgRasterizer.js +++ b/app/pencil-core/common/svgRasterizer.js @@ -20,6 +20,8 @@ Rasterizer.prototype.getImageDataFromUrl = function (url, callback) { }; Rasterizer.ipcBasedBackend = { + TIME_OUT: 10000, + pendingWorkMap: {}, init: function () { ipcRenderer.send("render-init", {}); }, @@ -27,7 +29,12 @@ Rasterizer.ipcBasedBackend = { var id = Util.newUUID(); ipcRenderer.once(id, function (event, data) { + var work = Rasterizer.ipcBasedBackend.pendingWorkMap[id]; + if (!work) return; + callback(parseLinks ? data : data.url); + window.clearTimeout(work.timeoutId); + delete Rasterizer.ipcBasedBackend.pendingWorkMap[id]; }); w = width * scale; @@ -42,6 +49,20 @@ Rasterizer.ipcBasedBackend = { var xml = Controller.serializer.serializeToString(svgNode); ipcRenderer.send("render-request", {svg: xml, width: w, height: h, scale: 1, id: id, processLinks: parseLinks}); + + var work = {}; + work.timeoutId = window.setTimeout(function () { + var work = Rasterizer.ipcBasedBackend.pendingWorkMap[id]; + if (!work) return; + callback(""); + delete Rasterizer.ipcBasedBackend.pendingWorkMap[id]; + + console.log("Rasterizer seems to be crashed, restarting now!!!"); + ipcRenderer.send("render-restart", {}); + + }, Rasterizer.ipcBasedBackend.TIME_OUT); + + Rasterizer.ipcBasedBackend.pendingWorkMap[id] = work; } }; Rasterizer.outProcessCanvasBasedBackend = { @@ -162,7 +183,23 @@ Rasterizer.prototype.rasterizePageToUrl = function (page, callback, scale, parse var s = (typeof (scale) == "undefined") ? 1 : scale; var f = function () { svg.setAttribute("page", page.name); - thiz.getBackend().rasterize(svg, page.width, page.height, s, callback, parseLinks); + var m = Pencil.controller.getDocumentPageMargin() || 0; + var w = page.width; + var h = page.height; + if (m) { + var g = svg.ownerDocument.createElementNS(PencilNamespaces.svg, "g"); + g.setAttribute("transform", translate(0 - m, 0 - m)); + while (svg.firstChild) { + g.appendChild(svg.removeChild(svg.firstChild)); + } + svg.appendChild(g); + + w -= 2 * m; + h -= 2 * m; + svg.setAttribute("width", w); + svg.setAttribute("height", h); + } + thiz.getBackend().rasterize(svg, w, h, s, callback, parseLinks); }; if (page.backgroundPage) { diff --git a/app/pencil-core/common/util.js b/app/pencil-core/common/util.js index bf33bf21..1893f27b 100644 --- a/app/pencil-core/common/util.js +++ b/app/pencil-core/common/util.js @@ -657,6 +657,10 @@ Dom.parseToNode = function (xml, dom) { return node; } Dom.parseDocument = function (xml) { + if (xml && xml.charCodeAt(0) === 0xFEFF) { + xml = xml.substr(1); + } + var dom = Dom.parser.parseFromString(xml, "text/xml"); return dom; }; @@ -1091,7 +1095,21 @@ Svg.getHeight = function (dom) { } return 0; }; - +Svg.SYMBOL_NAME_ATTR = "symbolName"; +Svg.getSymbolName = function (node) { + if (node.hasAttributeNS(PencilNamespaces.p, Svg.SYMBOL_NAME_ATTR)) { + return node.getAttributeNS(PencilNamespaces.p, Svg.SYMBOL_NAME_ATTR); + } else { + return null; + } +}; +Svg.setSymbolName = function (node, name) { + if (typeof(name) === "undefined" || name === null) { + node.remoteAttributeNS(PencilNamespaces.p, Svg.SYMBOL_NAME_ATTR); + } else { + return node.setAttributeNS(PencilNamespaces.p, Svg.SYMBOL_NAME_ATTR, name); + } +}; var Local = {}; Local.getInstalledFonts = function () { @@ -2136,6 +2154,43 @@ Util.importSandboxFunctions = function () { pencilSandbox[f.name] = f; } }; +Util.workOnListAsync = function (list, worker, callback) { + var index = -1; + var next = function () { + index ++; + if (!list || index >= list.length) { + if (callback) callback(); + return; + } + + var item = list[index]; + worker(item, index, next); + } + next(); +}; +Util.compareVersion = function (version1, version2) { + var a = version1.split(/\./); + var b = version2.split(/\./); + + for (var i = 0; i < Math.min(a.length, b.length); i ++) { + var n1 = parseInt(a[i], 10); + var n2 = parseInt(b[i], 10); + + if (isNaN(n1) || isNaN(n2)) { + n1 = a[i]; + n2 = b[i]; + } + + if (n1 > n2) return 1; + if (n1 < n2) return -1; + } + + if (a.length > b.length) return 1; + if (a.length < b.length) return -1; + + return 0; +}; + function pEval (expression, extra, codeLocation) { var result = null; @@ -2147,7 +2202,7 @@ function pEval (expression, extra, codeLocation) { result = eval(expression) } } catch (ex) { - if (expression.length < 1000) error("Problematic code: " + expression); + if (expression.length < 2000) error("Problematic code: " + expression); if (codeLocation) error("Code location: " + codeLocation); Console.dumpError(ex); } @@ -2341,7 +2396,7 @@ function copyFileSync(source, target) { fs.writeFileSync(targetFile, fs.readFileSync(source)); } -function copyFolderRecursiveSync( source, target ) { +function copyFolderRecursiveSync(source, target) { var files = []; //check if folder needs to be created or integrated @@ -2391,4 +2446,31 @@ function _after(fn, after) { }; } +function getRequiredValue(input, message, pattern) { + var value = input.value; + var valid = pattern ? value.match(pattern) : value.trim().length > 0; + if (!valid) { + var e = new Error(message || "Please enter a valid value."); + e._input = input; + e._isValidationError = true; + throw e; + } + + return value; +} +function handleCommonValidationError(e) { + if (e._isValidationError) { + Dialog.error(e.message, "", function () { + setTimeout(function () { + e._input.focus(); + e._input.select(); + }, 250); + }); + + return false; + } else { + throw e; + } +} + Util.importSandboxFunctions(geo_buildQuickSmoothCurve, geo_buildSmoothCurve, geo_getRotatedPoint, geo_pointAngle, geo_rotate, geo_translate, geo_vectorAngle, geo_vectorLength, geo_findIntersection); diff --git a/app/pencil-core/definition/collectionManager.js b/app/pencil-core/definition/collectionManager.js index 127e1dbf..65a01e57 100644 --- a/app/pencil-core/definition/collectionManager.js +++ b/app/pencil-core/definition/collectionManager.js @@ -152,8 +152,12 @@ CollectionManager._loadUserDefinedStencilsIn = function (stencilDir, excluded, i continue; } var folderPath = path.join(stencilDir, definitionFile); - if (CollectionManager._loadStencil(folderPath, parser, isSystem ? true : false, isDeveloperStencil ? true : false)) { - count++; + try { + if (CollectionManager._loadStencil(folderPath, parser, isSystem ? true : false, isDeveloperStencil ? true : false)) { + count++; + } + } catch (e) { + console.error(e); } } } catch (e) { diff --git a/app/pencil-core/definition/collectionRepository.js b/app/pencil-core/definition/collectionRepository.js index 69837689..8d3dd88d 100644 --- a/app/pencil-core/definition/collectionRepository.js +++ b/app/pencil-core/definition/collectionRepository.js @@ -5,14 +5,13 @@ const STENCILS_REPO_URL = "https://raw.githubusercontent.com/mbrainiac/stencils- var CollectionRepository = { }; -CollectionRepository.loadCollections = function() { +CollectionRepository.loadCollections = function(url) { return QP.Promise(function(resolve, reject) { var nugget = require("nugget"); var tempDir = tmp.dirSync({ keep: false, unsafeCleanup: true }).name; var filename = "repository-" + new Date().getTime() + ".xml"; - console.log('Downloading', STENCILS_REPO_URL, 'to', tempDir, filename); var nuggetOpts = { target: filename, dir: tempDir, @@ -20,7 +19,7 @@ CollectionRepository.loadCollections = function() { quiet: true }; - nugget(STENCILS_REPO_URL, nuggetOpts, function (errors) { + nugget(url || STENCILS_REPO_URL, nuggetOpts, function (errors) { if (errors) { var error = errors[0] // nugget returns an array of errors but we only need 1st because we only have 1 url if (error.message.indexOf('404') === -1) { @@ -75,6 +74,15 @@ CollectionRepository.parse = function(dom, url) { collections.push(CollectionRepository.parseCollection(node)); }); + //the one with uppercase "C" is intended for newer version with support for version checking + Dom.workOn("./p:Collection", collectionsNode, function (node) { + var minVersion = node.getAttribute("required-version"); + if (minVersion) { + if (Util.compareVersion(pkgInfo.version, minVersion) < 0) return; + } + collections.push(CollectionRepository.parseCollection(node)); + }); + _.forEach(collections, function(c) { var existed = _.find(CollectionManager.shapeDefinition.collections, function(e) { return e.id == c.id; diff --git a/app/pencil-core/definition/shapeDef.js b/app/pencil-core/definition/shapeDef.js index 362c5551..51ca2e63 100644 --- a/app/pencil-core/definition/shapeDef.js +++ b/app/pencil-core/definition/shapeDef.js @@ -21,6 +21,35 @@ ShapeDef.prototype.toString = function () { ShapeDef.prototype.getProperty = function (name) { return this.propertyMap[name]; }; +ShapeDef.prototype.removeProperty = function (name) { + var found = false; + for (var group of this.propertyGroups) { + for (var i = 0; i < group.properties.length; i ++) { + var property = group.properties[i]; + if (property.name == name) { + group.properties.splice(i, 1); + found = true; + break; + } + } + + if (found) break; + } + + if (found) { + delete this.propertyMap[name]; + } +}; +ShapeDef.prototype.removeAction = function (id) { + for (var i = 0; i < this.actions.length; i ++) { + var action = this.actions[i]; + if (action.id == id) { + this.actions.splice(i, 1); + delete this.actionMap[id]; + break; + } + } +}; ShapeDef.prototype.isPropertyAffectedBy = function (target, source, checkedProperties) { if (target == source) return true; @@ -51,6 +80,15 @@ PropertyGroup.prototype.toString = function () { return "[PropertyGroup: " + this.name + "]"; }; +PropertyGroup.prototype.clone = function () { + var group = new PropertyGroup(); + group.name = this.name; + for (var prop of this.properties) { + group.properties.push(prop.clone()); + } + + return group; +}; function Property() { this.name = null; @@ -64,6 +102,29 @@ function Property() { Property.prototype.toString = function () { return "[Property: " + this.name + "]"; }; +Property.prototype.clone = function () { + var property = new Property(); + property.name = this.name; + property.displayName = this.displayName; + property.type = this.type; + property.initialValue = this.initialValue ? this.type.fromString(this.initialValue.toString()) : null; + property.initialValueExpression = this.initialValueExpression; + + for (var name in this.relatedTargets) { + property.relatedTargets[name] = this.relatedTargets[name]; + } + + property.relatedProperties = {}; + for (var name in this.relatedProperties) { + property.relatedProperties[name] = this.relatedProperties[name]; + } + + for (var name in this.meta) { + property.meta[name] = this.meta[name]; + } + + return property; +}; Property.prototype.isSimilarTo = function (property) { return this.name == property.name && this.type == property.type; @@ -99,7 +160,7 @@ function BehaviorItemArg(literal, shapeDef, currentTarget, type) { this.literal = this.literal.replace(/\$([a-z][a-z0-9]*)/gi, function (zero, one) { var property = shapeDef.getProperty(one); if (!property) { - throw Util.getMessage("invalid.property.reference", one); + throw Util.getMessage("invalid.property.reference", one) + " (" + shapeDef.id + ")"; } property.relatedTargets[currentTarget] = true; return "properties." + one; @@ -127,13 +188,3 @@ function Shortcut() { this.shape = null; this.propertyMap = {}; } - - - - - - - - - - diff --git a/app/pencil-core/definition/shapeDefCollectionParser.js b/app/pencil-core/definition/shapeDefCollectionParser.js index f576f4f2..1b379cd0 100644 --- a/app/pencil-core/definition/shapeDefCollectionParser.js +++ b/app/pencil-core/definition/shapeDefCollectionParser.js @@ -138,7 +138,7 @@ ShapeDefCollectionParser.getCollectionPropertyConfigName = function (collectionI ShapeDefCollectionParser.prototype.loadCustomLayout = function (installDirPath) { var layoutUri = path.join(installDirPath, "Layout.xhtml"); if (!fs.existsSync(layoutUri)) return null; - + try { var html = fs.readFileSync(layoutUri, {encoding: "utf8"}); if (!html) return null; @@ -332,18 +332,51 @@ ShapeDefCollectionParser.prototype.loadCustomLayout = function (installDirPath) shapeDef.displayName = shapeDefNode.getAttribute("displayName"); shapeDef.system = shapeDefNode.getAttribute("system") == "true"; shapeDef.collection = collection; + var inherits = shapeDefNode.getAttribute("inherits"); + if (inherits) { + if (inherits.indexOf(":") < 0) inherits = collection.id + ":" + inherits; + var parentShapeDef = collection.shapeDefMap[inherits]; + if (parentShapeDef) { + shapeDef.parentShapeDef = parentShapeDef; + this.processInheritance(shapeDef); + } + } + var iconPath = shapeDefNode.getAttribute("icon"); // if (iconPath.indexOf("data:image") != 0) { // iconPath = collection.url.substring(0, collection.url.lastIndexOf("/") + 1) + iconPath; // } shapeDef.iconPath = iconPath; + // adding shapeDef meta + shapeDef.meta = {}; + Dom.workOn("./@p:*", shapeDefNode, function (metaAttribute) { + var metaValue = metaAttribute.nodeValue; + metaValue = metaValue.replace(/\$([a-z][a-z0-9]*)/gi, function (zero, one) { + property.relatedProperties[one] = true; + return "properties." + one; + }); + shapeDef.meta[metaAttribute.localName] = metaValue; + }); + var parser = this; //parse properties Dom.workOn("./p:Properties/p:PropertyGroup", shapeDefNode, function (propGroupNode) { - var group = new PropertyGroup; - group.name = propGroupNode.getAttribute("name"); + //find existing property group to support duplicate inherited groups + var groupName = propGroupNode.getAttribute("name"); + var group = null; + for (var g of shapeDef.propertyGroups) { + if (g.name == groupName) { + group = g; + break; + } + } + if (!group) { + group = new PropertyGroup(); + group.name = groupName; + shapeDef.propertyGroups.push(group); + } Dom.workOn("./p:Property", propGroupNode, function (propNode) { var property = new Property(); @@ -389,11 +422,13 @@ ShapeDefCollectionParser.prototype.loadCustomLayout = function (installDirPath) property.meta[metaAttribute.localName] = metaValue; }); + if (shapeDef.propertyMap[property.name]) { + shapeDef.removeProperty(property.name); + } + group.properties.push(property); shapeDef.propertyMap[property.name] = property; }); - - shapeDef.propertyGroups.push(group); }); /*/ styles @@ -514,6 +549,10 @@ ShapeDefCollectionParser.prototype.loadCustomLayout = function (installDirPath) Console.dumpError(e); } + if (shapeDef.actionMap[action.id]) { + shapeDef.removeAction(action.id); + } + shapeDef.actionMap[action.id] = action; shapeDef.actions.push(action); @@ -529,8 +568,52 @@ ShapeDefCollectionParser.prototype.loadCustomLayout = function (installDirPath) node.removeAttribute("id"); }); + + var parentContentPlaceHolder = Dom.getSingle(".//p:ParentContent", shapeDef.contentNode); + if (parentContentPlaceHolder && shapeDef.parentShapeDef && shapeDef.parentShapeDef.contentNode) { + var f = shapeDef.contentNode.ownerDocument.createDocumentFragment(); + for (var i = 0; i < shapeDef.parentShapeDef.contentNode.childNodes.length; i ++) { + var child = shapeDef.parentShapeDef.contentNode.childNodes[i]; + child = shapeDef.contentNode.ownerDocument.importNode(child, true); + f.appendChild(child); + } + + parentContentPlaceHolder.parentNode.replaceChild(f, parentContentPlaceHolder); + } + return shapeDef; }; + +/* public ShapeDef */ ShapeDefCollectionParser.prototype.processInheritance = function (shapeDef) { +// this.contentNode = null; +// this.propertyGroups = []; +// this.behaviors = []; +// this.actions = []; +// +// this.propertyMap = {}; +// this.behaviorMap = {}; +// this.actionMap = {}; + + shapeDef.propertyGroups = [].concat(); + for (var group of shapeDef.parentShapeDef.propertyGroups) { + var clonedGroup = group.clone(); + shapeDef.propertyGroups.push(clonedGroup); + + for (var prop of clonedGroup.properties) { + shapeDef.propertyMap[prop.name] = prop; + } + } + + shapeDef.behaviors = [].concat(shapeDef.parentShapeDef.behaviors); + for (var name in shapeDef.parentShapeDef.behaviorMap) { + shapeDef.behaviorMap[name] = shapeDef.parentShapeDef.behaviorMap[name]; + } + + shapeDef.actions = [].concat(shapeDef.parentShapeDef.actions); + for (var name in shapeDef.parentShapeDef.actionMap) { + shapeDef.actionMap[name] = shapeDef.parentShapeDef.actionMap[name]; + } +}; /* public Shortcut */ ShapeDefCollectionParser.prototype.parseShortcut = function (shortcutNode, collection) { var shortcut = new Shortcut(); diff --git a/app/pencil-core/editor/handleEditor.js b/app/pencil-core/editor/handleEditor.js index 4c9dfd20..58cec8dd 100644 --- a/app/pencil-core/editor/handleEditor.js +++ b/app/pencil-core/editor/handleEditor.js @@ -289,7 +289,6 @@ HandleEditor.prototype.handleMoveTo = function (x, y, event) { this.lastMatchedOutlet = null; } - console.log("move handle"); }; HandleEditor.prototype.getPropertyConstraints = function (handle) { if (!this.currentHandle) return {}; diff --git a/app/pencil-core/propertyType/bound.js b/app/pencil-core/propertyType/bound.js index 4b10138c..846fe7af 100644 --- a/app/pencil-core/propertyType/bound.js +++ b/app/pencil-core/propertyType/bound.js @@ -37,7 +37,7 @@ Bound.prototype.narrowed = function (x, y) { }; Bound.prototype.shifted = function (dx, dy) { return new Bound(this.x - dx, this.y - dy, this.w, this.h); - + }; pencilSandbox.Bound = { @@ -47,4 +47,4 @@ pencilSandbox.Bound = { }; for (var p in Bound) { pencilSandbox.Bound[p] = Bound[p]; -}; \ No newline at end of file +}; diff --git a/app/pencil-core/propertyType/color.js b/app/pencil-core/propertyType/color.js index 174248d4..fd56fbf4 100644 --- a/app/pencil-core/propertyType/color.js +++ b/app/pencil-core/propertyType/color.js @@ -174,6 +174,41 @@ Color.prototype.transparent = function () { return color; }; +Color.prototype.generateTransformTo = function (other) { + if (!other) return null; + var hsv0 = Color.RGB2HSV(this); + var hsv1 = Color.RGB2HSV(other); + + const ALLOWED_DELTA = 5; + if (Math.abs(hsv0.hue - hsv1.hue) < ALLOWED_DELTA && Math.abs(hsv0.saturation - hsv1.saturation) < ALLOWED_DELTA) { + var transform = ""; + + if (Math.abs(hsv0.value - hsv1.value) >= ALLOWED_DELTA / 3) { + if (hsv0.value == 0) return null; + transform += ".shaded(" + (1 - (hsv1.value / hsv0.value)) + ")"; + } + + if (Math.abs(this.a - other.a) >= 0.05) { + if (this.a == 0) return null; + transform += ".hollowed(" + (1 - (other.a / this.a)) + ")"; + } + + return transform; + } + + return null; +} +Color.prototype.getDiff = function (other) { + if (!other) return 1; + var hsv0 = Color.RGB2HSV(this); + var hsv1 = Color.RGB2HSV(other); + + return (Math.abs(hsv0.hue - hsv1.hue) / (255 * 4)) + + (Math.abs(hsv0.saturation - hsv1.saturation) / (255 * 4)) + + (Math.abs(hsv0.value - hsv1.value) / (100 * 4)) + + (Math.abs(this.a - other.a) / 4); +} + pencilSandbox.Color = { newColor: function () { return new Color(); diff --git a/app/pencil-core/propertyType/font.js b/app/pencil-core/propertyType/font.js index 2783b6fe..0795621d 100644 --- a/app/pencil-core/propertyType/font.js +++ b/app/pencil-core/propertyType/font.js @@ -48,6 +48,44 @@ Font.prototype.getFamilies = function () { } return families; } +Font.prototype.bold = function (yes) { + var font = Font.fromString(this.toString()); + font.weight = (typeof(yes) == "undefined" || yes) ? "bold" : "normal"; + + return font; +}; +Font.prototype.italic = function (yes) { + var font = Font.fromString(this.toString()); + font.style = (typeof(yes) == "undefined" || yes) ? "italic" : "normal"; + + return font; +}; +Font.prototype.resized = function (delta) { + var font = Font.fromString(this.toString()); + if (typeof(delta) == "string" && delta.match(/^(.+)%$/)) { + font.size = Math.round(this.getPixelHeight() * (1 + parseFloat(RegExp.$1) / 100)) + "px"; + } else if (typeof(delta) == "number") { + font.size = Math.round(this.getPixelHeight() * (1 + delta)) + "px"; + } + + return font; +}; +Font.prototype.generateTransformTo = function (other) { + if (this.family != other.family) return null; + + var transform = ""; + if (this.weight != other.weight) { + transform += ".bold(" + (this.weight != "bold") + ")"; + } + if (this.style != other.style) { + transform += ".italic(" + (this.style != "italic") + ")"; + } + if (this.size != other.size && this.getPixelHeight() > 0) { + transform += ".resized(" + ((other.getPixelHeight() / this.getPixelHeight()) - 1) + ")"; + } + + return transform; +}; pencilSandbox.Font = { newFont: function () { diff --git a/app/pencil-core/propertyType/imageData.js b/app/pencil-core/propertyType/imageData.js index b2aa8987..f93f091b 100644 --- a/app/pencil-core/propertyType/imageData.js +++ b/app/pencil-core/propertyType/imageData.js @@ -136,12 +136,12 @@ ImageData.refStringToUrl = function (refString) { return Pencil.controller.refIdToUrl(id); }; -ImageData.prompt = function (callback) { - dialog.showOpenDialog({ +ImageData.prompt = function (callback, ext) { + dialog.showOpenDialog(remote.getCurrentWindow(), { title: "Select Image", defaultPath: os.homedir(), filters: [ - { name: "Image files", extensions: ["png", "jpg", "jpeg", "gif", "bmp", "svg"] } + { name: "Image files", extensions: ext || ["png", "jpg", "jpeg", "gif", "bmp", "svg"] } ] }, function (filenames) { @@ -217,6 +217,27 @@ ImageData.prototype.toString = function () { return [this.w, this.h, ImageData.generateCellString(this.xCells), ImageData.generateCellString(this.yCells), this.data].join(","); } }; +ImageData.SVG_IMAGE_DATA_PREFIX = "data:image/svg+xml"; +ImageData.prototype.getDataAsXML = function () { + var url = this.data; + if (!url) return null; + + if (url.startsWith(ImageData.SVG_IMAGE_DATA_PREFIX)) { + var commaIndex = url.indexOf(","); + if (commaIndex < ImageData.SVG_IMAGE_DATA_PREFIX.length || commaIndex > ImageData.SVG_IMAGE_DATA_PREFIX.length + 10) return null; + var svg = url.substring(commaIndex + 1); + + return svg; + } else if (url.match(/^ref:\/\//)) { + var id = ImageData.refStringToId(url); + if (!id) { + return null; + } + var filePath = Pencil.controller.refIdToFilePath(id); + return fs.readFileSync(filePath, "utf8"); + } + return null; +}; window.addEventListener("load", function () { var iframe = document.createElementNS(PencilNamespaces.html, "html:iframe"); diff --git a/app/pencil-core/propertyType/strokeStyle.js b/app/pencil-core/propertyType/strokeStyle.js index 91c049e1..350e92db 100644 --- a/app/pencil-core/propertyType/strokeStyle.js +++ b/app/pencil-core/propertyType/strokeStyle.js @@ -1,6 +1,6 @@ function StrokeStyle(w, array) { - this.w = w ? w : 1; - this.array = array ? array : null; + this.w = (typeof(w) == "number") ? w : 1; + this.array = typeof(array) != "undefined" ? array : null; } StrokeStyle.REG_EX = /^([0-9]+)\|([0-9 \,]*)$/; StrokeStyle.fromString = function(literal) { @@ -17,8 +17,24 @@ StrokeStyle.prototype.toString = function () { return this.w + "|" + this.array; }; StrokeStyle.prototype.condensed = function (ratio) { - return new StrokeStyle(this.w * (1 + ratio), this.array); + return new StrokeStyle(Math.round(this.w * (1 + ratio)), this.array); }; +StrokeStyle.prototype.styled = function (array) { + return new StrokeStyle(this.w, array); +}; +StrokeStyle.prototype.generateTransformTo = function (other) { + var transform = ""; + + if (this.w != other.w && this.w > 0) { + transform += ".condensed(" + ((other.w / this.w) - 1) + ")"; + } + if (this.array != other.array) { + transform += ".styled(" + JSON.stringify(other.array) + ")"; + } + + return transform; +}; + pencilSandbox.StrokeStyle = { newStrokeStyle: function (w, array) { diff --git a/app/pencil-core/target/group.js b/app/pencil-core/target/group.js index 32a21db4..e3fdc20b 100644 --- a/app/pencil-core/target/group.js +++ b/app/pencil-core/target/group.js @@ -86,7 +86,7 @@ Group.prototype.getProperty = function (name) { //TODO: add additonal info to indicate sameness return firstValue; - + // if (!firstValue) return null; // var same = true; // for (var i = 1; i < this.targets.length; i ++) { @@ -523,3 +523,9 @@ Group.openSizingPolicyDialog = function (target) { }); }; +Group.prototype.getSymbolName = function () { + return Svg.getSymbolName(this.svg); +}; +Group.prototype.setSymbolName = function (name) { + return Svg.setSymbolName(this.svg, name); +}; diff --git a/app/pencil-core/target/shape.js b/app/pencil-core/target/shape.js index 34c69eb6..87c62838 100644 --- a/app/pencil-core/target/shape.js +++ b/app/pencil-core/target/shape.js @@ -1,4 +1,4 @@ -function Shape(canvas, svg) { +function Shape(canvas, svg, forcedDefinition) { this.svg = svg; this.canvas = canvas; @@ -9,7 +9,7 @@ function Shape(canvas, svg) { this.id = this.svg.getAttribute("id"); var defId = this.canvas.getType(svg); - this.def = CollectionManager.shapeDefinition.locateDefinition(defId); + this.def = forcedDefinition || CollectionManager.shapeDefinition.locateDefinition(defId); if (!this.def) { throw Util.getMessage("shape.definition.not.found", defId); } @@ -49,6 +49,7 @@ Shape.prototype.getPropertyGroups = function () { Shape.prototype.setInitialPropertyValues = function (overridingValueMap) { this._evalContext = {collection: this.def.collection}; + F._target = this.svg; var hasPostProcessing = false; @@ -133,6 +134,48 @@ Shape.prototype.repairShapeProperties = function () { this.applyBehaviorForProperty(name); } }; +Shape.prototype.renewTargetProperties = function () { + this._evalContext = {collection: this.def.collection}; + F._target = this.svg; + + var renewed = false; + + for (var name in this.def.propertyMap) { + var prop = this.def.propertyMap[name]; + if (!prop || !prop.meta || prop.meta.renew != "true") continue; + + var propNode = this.locatePropertyNode(name); + if (!propNode) continue; + + var value = null; + + var currentCollection = this.def.connection; + + if (prop.initialValueExpression) { + value = this.evalExpression(prop.initialValueExpression); + } else { + value = prop.initialValue; + } + + if (prop.type.performIntialProcessing) { + var newValue = prop.type.performIntialProcessing(value, this.def, currentCollection); + if (newValue) { + value = newValue; + } + } + + this.storeProperty(name, value); + renewed = true; + } + + if (!renewed) return false; + + for (name in this.def.propertyMap) { + this.applyBehaviorForProperty(name); + } + + return true; +}; Shape.prototype.applyBehaviorForProperty = function (name, dontValidateRelatedProperties) { var propertyDef = this.def.propertyMap[name]; if (!propertyDef) return; @@ -336,7 +379,7 @@ Shape.prototype.setProperty = function (name, value, nested) { Shape.prototype.getProperty = function (name) { var propNode = this.locatePropertyNode(name); if (!propNode) { - return null; + //return null; var prop = this.def.getProperty(name); if (!this._evalContext) { this._evalContext = {collection: this.def.collection}; @@ -344,6 +387,10 @@ Shape.prototype.getProperty = function (name) { this._evalContext.collection = this.def.collection; } + if (!prop) { + return null; + } + if (prop.initialValueExpression) { return this.evalExpression(prop.initialValueExpression); } else { @@ -1111,6 +1158,12 @@ Shape.prototype.invalidateInboundConnections = function () { Shape.prototype.invalidateOutboundConnections = function () { Connector.invalidateOutboundConnectionsForShapeTarget(this); }; +Shape.prototype.getSymbolName = function () { + return Svg.getSymbolName(this.svg); +}; +Shape.prototype.setSymbolName = function (name) { + return Svg.setSymbolName(this.svg, name); +}; Shape.prototype.generateShortcutXML = function () { new PromptDialog().open({ title: "Shortcut", @@ -1199,3 +1252,28 @@ Shape.prototype.generateShortcutXML = function () { }.bind(this) }); }; + +Shape.prototype.getContentEditActions = function (event) { + var actions = []; + var editInfo = this.getTextEditingInfo(event); + if (editInfo) { + actions.push({ + type: "text", + editInfo: editInfo + }); + } + for (var action of this.def.actions) { + if (action.meta["content-action"] == "true") { + actions.push({ + type: "action", + actionId: action.id + }); + } + } + + return actions; +}; +Shape.prototype.handleOtherContentEditAction = function (action) { + if (action.type != "action") return; + this.performAction(action.actionId); +}; diff --git a/app/pencil-core/xferHelper/shapeXferHelper.js b/app/pencil-core/xferHelper/shapeXferHelper.js index 2a2a0dd9..f083cf97 100644 --- a/app/pencil-core/xferHelper/shapeXferHelper.js +++ b/app/pencil-core/xferHelper/shapeXferHelper.js @@ -36,6 +36,18 @@ ShapeXferHelper.prototype.handleData = function (dom) { this.canvas.drawingLayer.appendChild(shape); this.canvas.selectShape(shape); + if (this.canvas.currentController.renewTargetProperties) { + try { + var renewed = this.canvas.currentController.renewTargetProperties(); + if (renewed) { + this.canvas.selectNone(); + this.canvas.selectShape(shape); + } + } catch (e) { + console.error(e); + } + } + var rect = this.canvas.currentController.getBoundingRect(); var mx = 0; var my = 0; @@ -57,6 +69,7 @@ ShapeXferHelper.prototype.handleData = function (dom) { this.canvas.snappingHelper.updateSnappingGuide(this.canvas.currentController); }, this, Util.getMessage("action.create.shape", this.canvas.createControllerFor(shape).getName())); + this.canvas.invalidateEditors(); }; diff --git a/app/stencils/Common/Definition.xml b/app/stencils/Common/Definition.xml index 251fae74..ae6cf7da 100644 --- a/app/stencils/Common/Definition.xml +++ b/app/stencils/Common/Definition.xml @@ -1088,7 +1088,7 @@ 180,90 - + diff --git a/app/views/ApplicationPane.js b/app/views/ApplicationPane.js index 324abe4f..6fdd9cdb 100644 --- a/app/views/ApplicationPane.js +++ b/app/views/ApplicationPane.js @@ -299,3 +299,7 @@ ApplicationPane.prototype.toggleLeftPane = function () { this.leftSidePane.openLast(); } }; + +ApplicationPane.prototype.setContentVisible = function (visible) { + this.contentBody.style.visibility = visible ? "visible" : "hidden"; +}; diff --git a/app/views/EditPageNoteDialog.js b/app/views/EditPageNoteDialog.js index dd2ed916..0159a494 100644 --- a/app/views/EditPageNoteDialog.js +++ b/app/views/EditPageNoteDialog.js @@ -106,7 +106,7 @@ function EditPageNoteDialog () { return n.getAttribute && n.getAttribute("command"); }); if (!node) return; - var command = node.getAttribute("command");ns + var command = node.getAttribute("command"); var arg = node.hasAttribute("arg") ? node.getAttribute("arg") : undefined; if(command == "createlink") { var sel = window.document.getSelection(); @@ -203,7 +203,7 @@ function EditPageNoteDialog () { } thiz.runEditorCommand("formatBlock", value); }, false); - + } __extend(Dialog, EditPageNoteDialog); diff --git a/app/views/StartUpDocumentView.js b/app/views/StartUpDocumentView.js index 18ae63dd..6946bb44 100644 --- a/app/views/StartUpDocumentView.js +++ b/app/views/StartUpDocumentView.js @@ -36,7 +36,8 @@ function StartUpDocumentView() { if (doc.thumbPath) { handler(null, doc.thumbPath); } else { - Pencil.documentHandler.parseDocumentThumbnail(filePath, handler); + handler(null, ""); + //Pencil.documentHandler.parseDocumentThumbnail(filePath, handler); } } diff --git a/app/views/collections/CollectionResourceBrowserDialog.js b/app/views/collections/CollectionResourceBrowserDialog.js index 85f81b15..2bf8260d 100644 --- a/app/views/collections/CollectionResourceBrowserDialog.js +++ b/app/views/collections/CollectionResourceBrowserDialog.js @@ -17,18 +17,51 @@ function CollectionResourceBrowserDialog (collection, options) { this.prefixCombo.setItems(this.prefixes); + this.collectionCombo.renderer = function (item) { + return item.displayName; + }; + var availableCollections = [collection]; + var selectedCollection = collection; + for (var c of CollectionManager.shapeDefinition.collections) { + if (c.id == collection.id || !c.RESOURCE_LIST) continue; + + if (this.options.type) { + var found = false; + for (var resource of c.RESOURCE_LIST) { + if (!resource.type || resource.type == this.options.type) { + found = true; + break; + } + } + + if (!found) continue; + } + + availableCollections.push(c); + if (CollectionResourceBrowserDialog.lastCollectionId == c.id) { + selectedCollection = c; + } + } + + this.collectionCombo.setItems(availableCollections); + this.collectionCombo.selectItem(selectedCollection); + this.searchTimeout = null; var thiz = this; this.bind("input", function () { if (this.searchTimeout) { window.clearTimeout(this.searchTimeout); + this.searchTimeout = null; } this.searchTimeout = window.setTimeout(function () { thiz.search(); }, 500); }, this.filterInput); + this.bind("keydown", this.handleFilterKeyDown, this.filterInput); + this.bind("p:ItemSelected", this.search, this.prefixCombo); + this.bind("p:ItemSelected", this.changeCollection, this.collectionCombo); var ensureVisibleItemsContentFunction = function() { this.revealTimeout = null; @@ -43,15 +76,16 @@ function CollectionResourceBrowserDialog (collection, options) { }, this.resultContainer); this.bind("dblclick", this.handleItemDblClick, this.resultContainer); + this.bind("keydown", this.handleListKeyDown, this.resultContainer); + this.resultContainer.addEventListener("focus", this.handleListFocus.bind(this), true); - var optionCache = CollectionResourceBrowserDialog.optionCache[this.collection.id]; - if (optionCache) { - this.filterInput.value = optionCache.keyword || ""; - for (var p of this.prefixes) { - if (p.prefix == optionCache.prefix) { - this.prefixCombo.selectItem(p); - break; - } + this.invalidatePrefixList(); + + this.filterInput.value = CollectionResourceBrowserDialog.lastKeyword || ""; + for (var p of this.prefixCombo.items) { + if (p.prefix == CollectionResourceBrowserDialog.lastPrefix) { + this.prefixCombo.selectItem(p); + break; } } } @@ -60,6 +94,7 @@ __extend(Dialog, CollectionResourceBrowserDialog); CollectionResourceBrowserDialog.TYPE_SVG = "svg"; CollectionResourceBrowserDialog.TYPE_BITMAP = "bitmap"; CollectionResourceBrowserDialog.RETURN_IMAGEDATA = "ImageData"; +CollectionResourceBrowserDialog.RETURN_IMAGEDATA_SVG_GROUP = "ImageDataSVGGroup"; CollectionResourceBrowserDialog.RETURN_CONTENT= "Content"; CollectionResourceBrowserDialog.RETURN_DOMCONTENT= "Document"; @@ -71,8 +106,8 @@ CollectionResourceBrowserDialog.prototype.getDialogActions = function () { { type: "accept", title: "Select", run: function () { - if (this.callback) this.callback(); - return true; + if (this.selectedView) this.returnDataInSelectedView(); + return false; } } ] @@ -80,19 +115,40 @@ CollectionResourceBrowserDialog.prototype.getDialogActions = function () { CollectionResourceBrowserDialog.prototype.onShown = function () { this.filterInput.focus(); + this.filterInput.select(); this.search(); }; +CollectionResourceBrowserDialog.prototype.invalidatePrefixList = function () { + var collection = this.collectionCombo.getSelectedItem(); + var prefixes = []; + if (collection.id == this.collection.id) { + prefixes = this.prefixes; + } else { + for (var resource of collection.RESOURCE_LIST) { + if (!resource.type || resource.type == this.options.type) { + prefixes.push(resource); + } + } + } + this.prefixCombo.setItems(prefixes); +}; +CollectionResourceBrowserDialog.prototype.changeCollection = function () { + this.invalidatePrefixList(); + this.focusResult = true; + this.search(); +}; CollectionResourceBrowserDialog.prototype.search = function () { + this.searchTimeout = null; var keyword = this.filterInput.value.trim(); var items = []; - var dirPath = this.collection.installDirPath; + var collection = this.collectionCombo.getSelectedItem(); + var dirPath = collection.installDirPath; this.prefix = this.prefixCombo.getSelectedItem().prefix; - CollectionResourceBrowserDialog.optionCache[this.collection.id] = { - keyword: keyword, - prefix: this.prefix - }; + CollectionResourceBrowserDialog.lastCollectionId = collection.id; + CollectionResourceBrowserDialog.lastKeyword = keyword; + CollectionResourceBrowserDialog.lastPrefix = this.prefix; if (this.prefix) { var parts = this.prefix.trim().split("/"); @@ -105,19 +161,32 @@ CollectionResourceBrowserDialog.prototype.search = function () { .then(function (matched) { Dom.empty(this.resultContainer); var count = 0; + var previousview = null; for (var item of matched) { - //if (count ++ > 50) break; var view = Dom.newDOMElement({ _name: "div", _uri: PencilNamespaces.html, title: path.basename(item.name), + tabindex: "" + count, "class": "Item", _children: [ //imageViewSpec ] }); view._data = item; + view._index = count; + + if (previousview) previousview._next = view; + view._prev = previousview; + previousview = view; + this.resultContainer.appendChild(view); + count ++; + } + + if (this.focusResult) { + if (this.resultContainer.firstChild && this.resultContainer.firstChild.focus) this.resultContainer.firstChild.focus(); + this.focusResult = false; } this.ensureVisibleItemsContent(); @@ -126,6 +195,95 @@ CollectionResourceBrowserDialog.prototype.search = function () { console.error(err); }); }; +CollectionResourceBrowserDialog.prototype.handleListFocus = function (e) { + var view = Dom.findUpwardForNodeWithData(e.target, "_data"); + if (!view) return; + this.selectedView = view; +}; +CollectionResourceBrowserDialog.prototype.returnData = function (data) { + var collection = this.collectionCombo.getSelectedItem(); + + if (this.returnType == CollectionResourceBrowserDialog.RETURN_IMAGEDATA) { + var id = Pencil.controller.collectionResourceAsRefSync(collection, data.relativePath); + this.close(new ImageData(data.size.w, data.size.h, ImageData.idToRefString(id))); + + } else if (this.returnType == CollectionResourceBrowserDialog.RETURN_IMAGEDATA_SVG_GROUP) { + var svg = Dom.parseFile(data.path); + var g = svg.createElementNS(PencilNamespaces.svg, "g"); + while (svg.documentElement.firstChild) { + var child = svg.documentElement.firstChild; + svg.documentElement.removeChild(child); + g.appendChild(child); + } + + var xmlData = CollectionResourceBrowserDialog.SVG_IMAGE_DATA_PREFIX + Dom.serializeNode(g); + this.close(new ImageData(data.size.w, data.size.h, xmlData)); + } else if (this.returnType == CollectionResourceBrowserDialog.RETURN_CONTENT) { + this.close({ + size: data.size, + content: new PlainText(fs.readFileSync(data.path)) + }); + } else if (this.returnType == CollectionResourceBrowserDialog.RETURN_DOMCONTENT) { + this.close({ + size: data.size, + document: Dom.parseFile(data.path) + }); + } +}; +CollectionResourceBrowserDialog.prototype.returnDataInSelectedView = function () { + if (!this.selectedView || !this.selectedView._data) return; + this.returnData(this.selectedView._data); +}; +CollectionResourceBrowserDialog.prototype.handleFilterKeyDown = function (e) { + if (e.keyCode == DOM_VK_ENTER || e.keyCode == DOM_VK_RETURN) { + if (this.searchTimeout) { + this.focusResult = true; + } else { + if (this.resultContainer.firstChild && this.resultContainer.firstChild.focus) this.resultContainer.firstChild.focus(); + } + + e.stopPropagation(); + e.preventDefault(); + } +}; +CollectionResourceBrowserDialog.prototype.handleListKeyDown = function (e) { + var view = Dom.findUpwardForNodeWithData(e.target, "_data"); + if (!view) return; + + if (e.keyCode == DOM_VK_UP) { + for (var i = view._index - 1; i >= 0; i --) { + var other = view.parentNode.childNodes[i]; + if (other && other.offsetLeft == view.offsetLeft) { + other.focus(); + return; + } + } + } else if (e.keyCode == DOM_VK_DOWN) { + for (var i = view._index + 1; i < view.parentNode.childNodes.length; i ++) { + var other = view.parentNode.childNodes[i]; + if (other && other.offsetLeft == view.offsetLeft) { + other.focus(); + return; + } + } + } else if (e.keyCode == DOM_VK_LEFT) { + if (view._prev) { + view._prev.focus(); + return; + } + } else if (e.keyCode == DOM_VK_RIGHT) { + if (view._next) { + view._next.focus(); + return; + } + } else if (e.keyCode == DOM_VK_TAB) { + this.filterInput.focus(); + this.filterInput.select(); + e.preventDefault(); + } else if (e.keyCode == DOM_VK_ENTER || e.keyCode == DOM_VK_RETURN) { + this.returnData(view._data); + } +}; CollectionResourceBrowserDialog.prototype.getMatchingResources = function (dirPath, relativePath, keyword) { return new Promise(function (resolve, reject) { var items = []; @@ -184,7 +342,7 @@ CollectionResourceBrowserDialog.handleSVGObjectLoaded = function (e) { var w = svg.getAttribute("width"); var h = svg.getAttribute("height"); svg.setAttribute("viewBox", "0 0 " + w + " " + h); - object.parentNode._data.size = new Dimension(w, h); + object.parentNode._data.size = new Dimension(Math.round(parseFloat(w)), Math.round(parseFloat(h))); }; CollectionResourceBrowserDialog.handleImageLoaded = function (e) { var image = event.target; @@ -242,25 +400,12 @@ CollectionResourceBrowserDialog.prototype.ensureVisibleItemsContent = function ( this.showItem(node, shouldShow); } }; - +CollectionResourceBrowserDialog.SVG_IMAGE_DATA_PREFIX = "data:image/svg+xml;utf8,"; CollectionResourceBrowserDialog.prototype.handleItemDblClick = function (e) { var data = Dom.findUpwardForData (e.target, "_data"); if (!data) return; - if (this.returnType == CollectionResourceBrowserDialog.RETURN_IMAGEDATA) { - var id = Pencil.controller.collectionResourceAsRefSync(this.collection, data.relativePath); - this.close(new ImageData(data.size.w, data.size.h, ImageData.idToRefString(id))) - } else if (this.returnType == CollectionResourceBrowserDialog.RETURN_CONTENT) { - this.close({ - size: data.size, - content: new PlainText(fs.readFileSync(data.path)) - }); - } else if (this.returnType == CollectionResourceBrowserDialog.RETURN_DOMCONTENT) { - this.close({ - size: data.size, - document: Dom.parseFile(data.path) - }); - } + this.returnData(data); }; CollectionResourceBrowserDialog.open = function (collection, options, callback) { diff --git a/app/views/collections/CollectionResourceBrowserDialog.xhtml b/app/views/collections/CollectionResourceBrowserDialog.xhtml index 9f28c94d..d45ed4ce 100644 --- a/app/views/collections/CollectionResourceBrowserDialog.xhtml +++ b/app/views/collections/CollectionResourceBrowserDialog.xhtml @@ -25,6 +25,10 @@ cursor: pointer; position: relative; } + @resultContainer .Item:focus { + outline: none; + background: lighten(@selected_bg, 40%); + } @resultContainer .Item > object, @resultContainer .Item > img { width: 5ex; @@ -43,16 +47,30 @@ box-shadow: 0px 0px 4px @selected_bg; } + @collectionSelectionPane { + margin-bottom: 1ex; + padding-bottom: 1ex; + border-bottom: solid 1px rgba(0, 0, 0, 0.3); + } + + @collectionCombo { + font-weight: bold; + } + @prefixCombo { margin-left: 0.2em; } + + + + - + diff --git a/app/views/common/NotificationPopup.js b/app/views/common/NotificationPopup.js index 6bea9233..54cf4abb 100644 --- a/app/views/common/NotificationPopup.js +++ b/app/views/common/NotificationPopup.js @@ -13,7 +13,7 @@ function NotificationPopup() { __extend(Popup, NotificationPopup); NotificationPopup.prototype.setup = function (message, actionTitle, actionHandler) { - this.messagePane.innerHTML = Dom.htmlEncode(message); + this.messagePane.innerHTML = Dom.htmlEncode(message).replace(/\n/g, "
"); Dom.toggleClass(this.node(), "WithAction", actionTitle) if (actionTitle) { diff --git a/app/views/common/Popup.js b/app/views/common/Popup.js index d1b040a3..02f15e01 100644 --- a/app/views/common/Popup.js +++ b/app/views/common/Popup.js @@ -133,6 +133,8 @@ Popup.prototype.isVisible = function () { Popup.prototype.showAt = function (x, y, skipEvent, autoFlip) { this.reparent(); + console.log("Showing at: ", [x, y]); + if (this.mode) { this.popupContainer.setAttribute("mode", this.mode); } diff --git a/app/views/common/UICommandManager.js b/app/views/common/UICommandManager.js index 1ebc0aa3..d24f0347 100644 --- a/app/views/common/UICommandManager.js +++ b/app/views/common/UICommandManager.js @@ -137,7 +137,7 @@ window.document.addEventListener("focus", function (event) { }, true); -UICommandManager.register = function (command) { +UICommandManager.register = function (command, control) { command._run = command.run; command.run = UICommandManager.checkAndRunFunction; @@ -158,6 +158,12 @@ UICommandManager.register = function (command) { document.body.addEventListener(eventNames[i], f, false); } } + + if (control) { + UICommandManager.installControl(command.key, control); + } + + return command; }; UICommandManager.getCommand = function (commandKey) { if (!commandKey) return; @@ -179,7 +185,7 @@ UICommandManager.installControl = function (commandKey, control) { }; UICommandManager.invalidateCommand = function (command) { if (!command.controls) return; - var valid = command.isValid ? command.isValid() : !command.disabled; + var valid = command.isValid ? command.isValid() : (command.isAvailable ? command.isAvailable() : !command.disabled); for (var i = 0; i < command.controls.length; i ++) { command.controls[i].disabled = !valid; if (command.controls[i].setEnabled) command.controls[i].setEnabled(valid); @@ -243,6 +249,8 @@ UICommandManager.handleKeyEvent = function (event) { continue; } + if (command.isAvailable && !command.isAvailable()) continue; + var eventCmdKey = command.parsedShortcut.command ? event.metaKey : false; var eventCtrlKey = !command.parsedShortcut.command && IS_MAC ? event.metaKey : event.ctrlKey; if (eventCmdKey == command.parsedShortcut.command diff --git a/app/views/editors/NPatchSpecEditorDialog.js b/app/views/editors/NPatchSpecEditorDialog.js index c5592ddc..515ed674 100644 --- a/app/views/editors/NPatchSpecEditorDialog.js +++ b/app/views/editors/NPatchSpecEditorDialog.js @@ -7,6 +7,11 @@ function NPatchSpecEditorDialog () { this.bind("mousedown", this.handleGlobalMouseDown, this.yCellContainer); this.bind("mouseup", this.handleGlobalMouseUp, document); this.bind("mousemove", this.handleGlobalMouseMove, document); + + this.bind("click", function () { + console.log(this.useDarkBackgroundCheckbox.checked); + this.container.setAttribute("dark", this.useDarkBackgroundCheckbox.checked); + }, this.useDarkBackgroundCheckbox); } __extend(Dialog, NPatchSpecEditorDialog); @@ -146,8 +151,66 @@ NPatchSpecEditorDialog.prototype.invalidateCellPosition = function (cell) { } var info = cell._cellInfo; if (info) { - info.textContent = b - a; + info.textContent = (cell._data.to - cell._data.from); + if (!cell._isX) { + var w = Math.round(info.offsetWidth); + var r = (s/2) - (w/2); + info.style.transform = "rotate(-90deg) translate(-" + r + "px, 0px)"; + } + } +}; +NPatchSpecEditorDialog.commandsToData = function (pathCommands) { + var newData = ""; + + for (var command of pathCommands) { + if (newData) newData += " "; + newData += command.command; + if (command.points) { + for (var i = 0; i < command.points.length; i ++) { + newData += (i > 0 ? " " : "") + command.points[i].x + "," + command.points[i].y; + } + } + } + + return newData; +}; + +NPatchSpecEditorDialog.generatePathSVGData = function (svgPathData, size) { + var specs = []; + var json = svgPathData.data; + if (!json.startsWith("json:")) return specs; + var parsedPathData = JSON.parse(json.substring(5)); + + + for (var info of parsedPathData) { + var d = NPatchSpecEditorDialog.commandsToData(info.commands); + specs.push({ + _name: "path", + _uri: PencilNamespaces.svg, + d: d, + style: "stroke: #000000; stroke-width: 1px; fill: rgba(0, 0, 0, 0.1);" + }); } + + var svg = { + _name: "svg", + _uri: PencilNamespaces.svg, + width: svgPathData.w, + height: svgPathData.h, + viewBox: "0 0 " + size.w + " " + size.h, + _children: [ + { + _name: "g", + _uri: PencilNamespaces.svg, + //transform: scale(size.w / svgPathData.w, size.h / svgPathData.h), + _children: specs + } + ] + } + + var svgDom = Dom.newDOMElement(svg); + var svgData = encodeURIComponent(Dom.serializeNode(svgDom)); + return "data:image/svg+xml," + svgData; }; NPatchSpecEditorDialog.prototype.setup = function (options) { this.options = options || {}; @@ -158,7 +221,13 @@ NPatchSpecEditorDialog.prototype.setup = function (options) { var r0 = Math.max(this.options.imageData.w / maxW, this.options.imageData.h / maxH); var r1 = Math.min(this.options.imageData.w / minSize, this.options.imageData.h / minSize); - this.image.src = ImageData.refStringToUrl(this.options.imageData.data); + if (this.options.imageData.data && this.options.imageData.data.startsWith("json:")) { + this.image.src = NPatchSpecEditorDialog.generatePathSVGData(this.options.imageData, this.options.imageData); + } else if (this.options.imageData.data && this.options.imageData.data.startsWith("data:")) { + this.image.src = this.options.imageData.data; + } else { + this.image.src = ImageData.refStringToUrl(this.options.imageData.data); + } var r = r1 < 1 ? r1 : r0; this.setZoom(r); diff --git a/app/views/editors/NPatchSpecEditorDialog.xhtml b/app/views/editors/NPatchSpecEditorDialog.xhtml index 9ebfcdf0..49e9f244 100644 --- a/app/views/editors/NPatchSpecEditorDialog.xhtml +++ b/app/views/editors/NPatchSpecEditorDialog.xhtml @@ -52,7 +52,7 @@ height: 0.5em; cursor: ns-resize; } - @xCellContainer > .Cell > .StartResizer { + @yCellContainer > .Cell > .StartResizer { top: -0.25em; } @yCellContainer > .Cell > .EndResizer { @@ -91,8 +91,6 @@ } .CellContainer > .Cell > .YCellInfo > .Info { margin-left: 0.25em; - margin-top: 1em; - transform: rotate(-90deg); } @xCellContainer > .Cell > .Indicator { top: 1.5em; @@ -113,6 +111,10 @@ @container { align-items: center; justify-content: center; + margin-bottom: 1ex; + } + @container[dark='true'] { + background: #555; } @container > * { /* background: rgba(0, 0, 0, 0.2); */ @@ -134,4 +136,5 @@ + diff --git a/app/views/editors/OnMenuEditor.js b/app/views/editors/OnMenuEditor.js index e0791ba6..20f34e09 100644 --- a/app/views/editors/OnMenuEditor.js +++ b/app/views/editors/OnMenuEditor.js @@ -1,5 +1,25 @@ function OnMenuEditor() { + OnMenuEditor.globalSetup(); } + +OnMenuEditor.globalSetupDone = false; +OnMenuEditor.globalSetup = function () { + if (OnMenuEditor.globalSetupDone) return; + OnMenuEditor.globalSetupDone = true; + + OnMenuEditor.invokeSecondaryContentEditCommand = UICommandManager.register({ + key: "invokeSecondaryContentEditCommand", + label: "invokeSecondaryContentEditCommand", + shortcut: "Shift+F2", + isAvailable: function () { return Pencil.activeCanvas && Pencil.activeCanvas.currentController && Pencil.activeCanvas.currentController.handleOtherContentEditAction }, + run: function () { + var editActions = Pencil.activeCanvas.currentController.getContentEditActions(); + if (editActions.length < 2) return; + Pencil.activeCanvas.currentController.handleOtherContentEditAction(editActions[1]); + } + }); +}; + OnMenuEditor.prototype.install = function (canvas) { this.canvas = canvas; this.canvas.contextMenuEditor = this; @@ -17,6 +37,7 @@ OnMenuEditor.prototype.generateMenuItems = function () { if (!this.targetObject) return []; var definedGroups = this.targetObject.getPropertyGroups(); var items = []; + var importantItems = []; var thiz = this; var allowDisabled = Config.get("dev.enable_disabled_in_menu", null); @@ -63,6 +84,7 @@ OnMenuEditor.prototype.generateMenuItems = function () { } }; items.push(item); + importantItems.push(item); } else if (property.type == Enum) { var enumItem = { type: "SubMenu", @@ -90,7 +112,16 @@ OnMenuEditor.prototype.generateMenuItems = function () { }); } items.push(enumItem); - } else if (property.type == ImageData) { //TODO: property constraint to enable n-patch edit? + } else if (property.type == ImageData) { + if (this.targetObject.def) { + var meta = this.targetObject.def.propertyMap[property.name].meta["npatch-edit"]; + if (meta) { + var value = this.targetObject.evalExpression(meta, false); + if (!value) continue; + } else { + continue; + } + } var value = thiz.targetObject.getProperty(property.name); var imageNPathSpecEditItem = { @@ -119,12 +150,36 @@ OnMenuEditor.prototype.generateMenuItems = function () { previousImageDataMenu = imageNPathSpecEditItem; items.push(imageNPathSpecEditItem); + importantItems.push(imageNPathSpecEditItem); } } } + if (items.length > 10) { + var otherPropItem = { + label: "Other Properties", + type: "SubMenu", + subItems: [] + }; + + for (var item of items) { + if (importantItems.indexOf(item) < 0) { + otherPropItem.subItems.push(item); + } + } + + items = importantItems; + items.push(otherPropItem); + } + //actions var actionItem = null; + var editActions = this.targetObject.getContentEditActions ? this.targetObject.getContentEditActions() : []; + var primaryActionId = null; + var secondaryActionId = null; + if (editActions.length > 0 && editActions[0].type == "action") primaryActionId = editActions[0].actionId; + if (editActions.length > 1 && editActions[1].type == "action") secondaryActionId = editActions[1].actionId; + if (this.targetObject.def && this.targetObject.performAction) { for (var i in this.targetObject.def.actions) { var action = this.targetObject.def.actions[i]; @@ -144,10 +199,16 @@ OnMenuEditor.prototype.generateMenuItems = function () { subItems: [] } } + + var shortcut = null; + if (action.id == primaryActionId) shortcut = "F2"; + if (action.id == secondaryActionId) shortcut = OnMenuEditor.invokeSecondaryContentEditCommand.shortcut; + actionItem.subItems.push({ label: action.displayName, type: "Normal", actionId: action.id, + shortcut: shortcut, handleAction: function (){ thiz.targetObject.performAction(this.actionId); thiz.canvas.invalidateEditors(); diff --git a/app/views/editors/OnScreenRichTextEditor.js b/app/views/editors/OnScreenRichTextEditor.js index a052e927..25bf146a 100644 --- a/app/views/editors/OnScreenRichTextEditor.js +++ b/app/views/editors/OnScreenRichTextEditor.js @@ -2,6 +2,7 @@ function OnScreenRichTextEditor() { BaseTemplatedWidget.call(this); this.popup.allowMouseDragging = true; + this.acMenu = new Menu(); } __extend(BaseTemplatedWidget, OnScreenRichTextEditor); @@ -50,8 +51,10 @@ OnScreenRichTextEditor.prototype.install = function (canvas) { }, false); var thiz = this; + this.bind("keydown", this.handleACKeyDown, this.container); this.bind("keypress", this.handleKeyPress, this.container); this.bind("keyup", this.handleKeyPress, this.container); + this.bind("input", this.handleInput, this.container); }; OnScreenRichTextEditor.prototype.addEditorEvent = function (name, handler) { this.textEditor.addEventListener(name, handler, false); @@ -68,9 +71,25 @@ OnScreenRichTextEditor.prototype.dettach = function () { OnScreenRichTextEditor.prototype.handleShapeDoubleClicked = function (event) { this.currentTarget = event.controller; - if (!this.currentTarget || !this.currentTarget.getTextEditingInfo) return; + if (!this.currentTarget) return; - this.textEditingInfo = this.currentTarget.getTextEditingInfo(event); + this.textEditingInfo = null; + + if (this.currentTarget.getContentEditActions) { + var actions = this.currentTarget.getContentEditActions(event); + if (actions.length == 0) return; + + var action = actions[0]; + if (action.type == "text") { + this.textEditingInfo = action.editInfo; + } else { + this.currentTarget.handleOtherContentEditAction(action); + return; + } + } else { + if (!this.currentTarget.getTextEditingInfo) return; + this.textEditingInfo = this.currentTarget.getTextEditingInfo(event); + } if (!this.textEditingInfo || this.textEditingInfo.readonly) return; @@ -170,13 +189,131 @@ OnScreenRichTextEditor.prototype._setupEditor = function () { thiz.textToolOverlay.node().style.visibility = "visible"; }, 10); }; +OnScreenRichTextEditor.MIN_AC_LENGTH = 5; +OnScreenRichTextEditor.prototype.handleInput = function (event) { + var selection = window.getSelection(); + if (!selection || !selection.anchorNode) { + this.hideAutoComplete(); + return; + } + if (!this.container.contains(selection.anchorNode)) { + this.hideAutoComplete(); + return; + } + if (selection.anchorOffset < OnScreenRichTextEditor.MIN_AC_LENGTH) { + this.hideAutoComplete(); + return; + } + + var textContent = selection.anchorNode.textContent; + var text = textContent.substring(selection.anchorOffset - OnScreenRichTextEditor.MIN_AC_LENGTH, selection.anchorOffset); + var acSet = this.getAutoCompleteSet(text, textContent == text); + if (acSet && acSet.items && acSet.items.length > 0) { + this.showAutoComplete(acSet, selection.anchorNode, selection.anchorOffset); + } else { + this.hideAutoComplete(); + } +}; + +OnScreenRichTextEditor.prototype.showAutoComplete = function (set, node, offset) { + this.acMenu.items = []; + function createItem(acItem) { + var label = (typeof(acItem) == "string" ? acItem : (acItem.label || acItem.toString())); + if (label.length > 40) label = label.substring(0, 40) + "... (" + label.length + " characters)"; + return { + label: label, + run: function () { + var text = typeof(acItem) == "string" ? acItem : (acItem.content || acItem.toString()); + + var textContent = node.textContent; + var start = Math.max(0, offset - set.replacementSize); + var newText = textContent.substring(0, start) + text + textContent.substring(offset); + node.textContent = newText; + var selection = window.getSelection(); + selection.collapseToStart(); + selection.extend(node, start + text.length); + selection.collapseToEnd(); + } + }; + } + for (var acItem of set.items) { + this.acMenu.register(createItem(acItem)); + } + + var anchorNode = node; + if (!anchorNode.localName) anchorNode = anchorNode.parentNode; + this.acSelectedItemIndex = -1; + this.acMenu.showMenu(anchorNode, "left-inside", "bottom", 0, 5, true); +}; +OnScreenRichTextEditor.prototype.hideAutoComplete = function () { + this.acMenu.hideMenu(); +}; + +//TODO: Refactor this into AC set registration from stencil +OnScreenRichTextEditor.prototype.getAutoCompleteSet = function (term, isFull) { + if (term && term.toLowerCase() == "lorem") { + var up = term.substring(0, 1) == "L"; + return { + items: [ + up ? capitalize(getLoremWord()) : getLoremWord(), + up ? loremIpsumSentence(3).trim() : loremIpsum(3), + up ? loremIpsumSentence(5).trim() : loremIpsum(5), + up ? loremIpsumSentence(10).trim() : loremIpsum(10), + up ? loremIpsumSentence(20).trim() : loremIpsum(20), + loremIpsumParagraph(40) + ], + replacementSize: term.length + } + } + + return null; +}; +OnScreenRichTextEditor.prototype.handleACKeyDown = function (event) { + if (!this.acMenu.isVisible()) return; + if (event.keyCode == DOM_VK_UP || event.keyCode == DOM_VK_DOWN) { + var items = this.acMenu.getMenuItemNodes(); + if (event.keyCode == DOM_VK_DOWN) { + this.acSelectedItemIndex ++; + if (this.acSelectedItemIndex >= items.length) this.acSelectedItemIndex = 0; + } else { + this.acSelectedItemIndex --; + if (this.acSelectedItemIndex < 0) this.acSelectedItemIndex = items.length - 1; + } + + for (var i = 0; i < items.length; i ++) { + if (i == this.acSelectedItemIndex) { + items[i].setAttribute("selected", "true"); + } else { + items[i].removeAttribute("selected"); + } + } + + event.stopPropagation(); + event.preventDefault(); + } else if (event.keyCode == DOM_VK_RETURN || event.keyCode == DOM_VK_ENTER) { + var items = this.acMenu.getMenuItemNodes(); + if (this.acSelectedItemIndex >= items.length || this.acSelectedItemIndex < 0) return; + var item = items[this.acSelectedItemIndex]._item; + if (item && item.run) item.run(); + this.hideAutoComplete(); + + event.stopPropagation(); + event.preventDefault(); + + this.cancelNextCommit = true; + } +}; OnScreenRichTextEditor.prototype.handleKeyPress = function (event) { if (this.textToolOverlay.settingFont) { return; } if (event.keyCode == DOM_VK_RETURN && !event.shiftKey && !event.accelKey) { + if (this.cancelNextCommit) { + this.cancelNextCommit = false; + return; + } var insideList = null; try { var node = window.getSelection().anchorNode; @@ -185,7 +322,9 @@ OnScreenRichTextEditor.prototype.handleKeyPress = function (event) { }); } catch (e) {} - if (!insideList || event.ctrlKey) this.commitChange(event); + if (!insideList || event.ctrlKey) { + this.commitChange(event); + } } else if (event.keyCode == DOM_VK_ESCAPE && this.popup == BaseWidget.getTopClosable()) { this.cancelChange(); diff --git a/app/views/editors/PlainTextEditor.js b/app/views/editors/PlainTextEditor.js index d97cadd0..09fcfdb6 100644 --- a/app/views/editors/PlainTextEditor.js +++ b/app/views/editors/PlainTextEditor.js @@ -9,7 +9,6 @@ PlainTextEditor.prototype.setup = function () { thiz.fireChangeEvent(); }, false); this.textarea.addEventListener("keyup", function (event) { - console.log("keyup") thiz.fireChangeEvent(); }, false); }; diff --git a/app/views/editors/SharedGeomtryEditor.js b/app/views/editors/SharedGeomtryEditor.js index 520fd3cc..e40d2a7a 100644 --- a/app/views/editors/SharedGeomtryEditor.js +++ b/app/views/editors/SharedGeomtryEditor.js @@ -41,11 +41,40 @@ SharedGeomtryEditor.prototype.setup = function () { } }, false); + var thiz = this; + UICommandManager.register({ + key: "lockMovementCommand", + getLabel: function () { + return "Lock shape's movement"; + }, + shortcut: "F12", + run: function () { + var locked = thiz.lockMovementButton.getAttribute("checked") == "true"; + if (locked) { + thiz.lockMovementButton.removeAttribute("checked"); + } else { + thiz.lockMovementButton.setAttribute("checked", "true"); + } + + Pencil.controller.movementDisabled = !locked; + } + }, this.lockMovementButton); + this.container.ownerDocument.documentElement.addEventListener("p:ShapeGeometryModified", function (event) { if (event.setter && event.setter == thiz) return; thiz.invalidate(); }, false); }; +SharedGeomtryEditor.prototype.toggleMovementLocking = function () { + var locked = this.lockMovementButton.getAttribute("checked") == "true"; + if (locked) { + this.lockMovementButton.removeAttribute("checked"); + } else { + this.lockMovementButton.setAttribute("checked", "true"); + } + + Pencil.controller.movementDisabled = !locked; +}; SharedGeomtryEditor.prototype.handleCommandEvent = function () { var currentGeo = this.targetObject.getGeometry(); var dx = this.shapeX.value - currentGeo.ctm.e; @@ -125,8 +154,8 @@ SharedGeomtryEditor.prototype.attach = function (targetObject) { this.handleBox = null; - this.shapeX.value = Math.max(0, Math.round(geo.ctm.e)); - this.shapeY.value = Math.max(0, Math.round(geo.ctm.f)); + this.shapeX.value = Math.round(geo.ctm.e); + this.shapeY.value = Math.round(geo.ctm.f); this.shapeWidth.value = Math.round(geo.dim.w); this.shapeHeight.value = Math.round(geo.dim.h); diff --git a/app/views/editors/SharedGeomtryEditor.xhtml b/app/views/editors/SharedGeomtryEditor.xhtml index 7f88790d..ac3023ff 100644 --- a/app/views/editors/SharedGeomtryEditor.xhtml +++ b/app/views/editors/SharedGeomtryEditor.xhtml @@ -6,10 +6,14 @@ @container > * + * { margin-left: @toolbar_gap; } + @lockMovementButton[checked='true'] { + color: #F00; + } + diff --git a/app/views/editors/SharedPropertyEditor.js b/app/views/editors/SharedPropertyEditor.js index 598f5195..b73b531c 100644 --- a/app/views/editors/SharedPropertyEditor.js +++ b/app/views/editors/SharedPropertyEditor.js @@ -11,7 +11,6 @@ SharedPropertyEditor.prototype.setup = function () { var thiz = this; this.propertyContainer.addEventListener("p:ValueChanged", function(event) { - console.log("p:ValueChanged", event); if (!thiz.target) return; var editor = Dom.findUpward(event.target, function (n) { return n._property; @@ -30,6 +29,10 @@ SharedPropertyEditor.prototype.setup = function () { thiz.setDefaultProperties(); } }, false); + this.propertyContainer.addEventListener("input", function(event) { + if (event.target != thiz.symbolNameInput || !thiz.target || !thiz.target.setSymbolName) return; + thiz.target.setSymbolName(event.target.value.trim()); + }, false); }; SharedPropertyEditor.prototype.getTitle = function() { return "Properties"; @@ -45,17 +48,17 @@ SharedPropertyEditor.prototype.sizeChanged = function (expanded) { } } SharedPropertyEditor.prototype.validationEditorUI = function() { - if (!this.validationEditor) return ; + if (!this.validationEditor) return; + var allowDisabled = Config.get(Config.DEV_ENABLE_DISABLED_IN_PROP_PAGE); for (var i = 0; i < this.validationEditor.length; i++) { - this.validationEditor[i].style.display = "none"; var name = this.validationEditor[i]._property.name; var meta = this.target.def.propertyMap[name].meta["disabled"]; - var value = this.target.evalExpression(meta, true); + var disabled = !allowDisabled && this.target.evalExpression(meta, true); - if (!value) this.validationEditor[i].style.display = "inherit"; + this.validationEditor[i].style.display = disabled ? "none" : "flex"; } -} +}; SharedPropertyEditor.prototype.attach = function (target) { @@ -116,25 +119,13 @@ SharedPropertyEditor.prototype.attach = function (target) { var properties = []; - var allowDisabled = Config.get("dev.enable_disabled_in_property_page", null); - if (allowDisabled == null) { - Config.set("dev.enable_disabled_in_property_page", false); - allowDisabled = false; - } + var allowDisabled = Config.get(Config.DEV_ENABLE_DISABLED_IN_PROP_PAGE); for (var i in definedGroups) { var group = definedGroups[i]; for (var j in group.properties) { var property = group.properties[j]; - if (this.target.def) { - var meta = this.target.def.propertyMap[property.name].meta["disabled"]; - if (meta && !allowDisabled) { - var value = this.target.evalExpression(meta, true); - if (value) continue; - } - } - var editor = TypeEditorRegistry.getTypeEditor(property.type); if (!editor) continue; property._group = group; @@ -169,6 +160,27 @@ SharedPropertyEditor.prototype.attach = function (target) { }); thiz.propertyContainer.appendChild(hbox); } + + if (StencilCollectionBuilder.isDocumentConfiguredAsStencilCollection() && thiz.target.getSymbolName) { + thiz.propertyContainer.appendChild(Dom.newDOMElement({ + _name: "vbox", + "class": "SymbolNameContainer", + _children: [ + { + _name: "label", + _text: "Symbol Name:" + }, + { + _name: "input", + type: "text", + _id: "symbolNameInput", + value: thiz.target.getSymbolName() || "" + + } + ] + }, document, thiz)); + } + thiz.propertyContainer.style.display = "flex"; thiz.propertyContainer.style.opacity = "1"; thiz.validationEditorUI(); @@ -229,11 +241,17 @@ SharedPropertyEditor.prototype.attach = function (target) { thiz.propertyEditor[property.name] = editorWidget; editorWrapper._property = property; - if (property.reload) { + + var meta = property.meta["disabled"]; + + if (meta) { if (!thiz.validationEditor) thiz.validationEditor = []; thiz.validationEditor.push(editorWrapper); - editorWrapper.style.display = "none"; + + var disabled = !allowDisabled && thiz.target.evalExpression(meta, true); + editorWrapper.style.display = disabled ? "none" : "flex"; } + currentGroupNode.appendChild(editorWrapper); window.setTimeout(executor(), 40); }; diff --git a/app/views/editors/SharedPropertyEditor.xhtml b/app/views/editors/SharedPropertyEditor.xhtml index ab525ec2..688206c1 100644 --- a/app/views/editors/SharedPropertyEditor.xhtml +++ b/app/views/editors/SharedPropertyEditor.xhtml @@ -82,6 +82,14 @@ font-weight: bold; line-height: 2.4 } + @propertyContainer .SymbolNameContainer { + margin-top: 1em; + border-top: solid 1px rgba(0, 0, 0, 0.2); + padding-top: 1em; + } + @propertyContainer .SymbolNameContainer label { + margin-bottom: 0.3ex; + } Select objects in the canvas to edit properties. diff --git a/app/views/menus/MainMenu.js b/app/views/menus/MainMenu.js index 285911f0..472ecb89 100644 --- a/app/views/menus/MainMenu.js +++ b/app/views/menus/MainMenu.js @@ -73,6 +73,8 @@ MainMenu.prototype.setup = function () { if (Config.get("dev.enabled", null) == null) Config.set("dev.enabled", false); + var devEnable = Config.get("dev.enabled", false); + this.register(UICommandManager.getCommand("newDocumentCommand")); this.register(UICommandManager.getCommand("openDocumentCommand")); this.register(UICommandManager.getCommand("saveDocumentCommand")); @@ -93,15 +95,15 @@ MainMenu.prototype.setup = function () { } }); - var developerToolSubItems = []; - developerToolSubItems.push({ + var toolSubItems = []; + toolSubItems.push({ key: "manageCollections", label: "Manage Collections...", run: function () { new CollectionManagementDialog(Pencil.collectionPane).open(); } }); - developerToolSubItems.push({ + toolSubItems.push({ key: "manageExportTemplate", label: "Manage Export Template...", run: function () { @@ -109,7 +111,7 @@ MainMenu.prototype.setup = function () { templateDialog.open(); } }); - developerToolSubItems.push({ + toolSubItems.push({ key: "manageFontCommand", label: "Manage Fonts...", run: function () { @@ -117,7 +119,9 @@ MainMenu.prototype.setup = function () { } }); - developerToolSubItems.push(Menu.SEPARATOR); + toolSubItems.push(Menu.SEPARATOR); + + var developerToolSubItems = []; developerToolSubItems.push({ key: "stencilGenerator", label: "Stencil Generator...", @@ -158,12 +162,45 @@ MainMenu.prototype.setup = function () { Pencil.controller.exportAsLayout(); } }); + developerToolSubItems.push(Menu.SEPARATOR); + + developerToolSubItems.push(UICommandManager.register({ + key: "configureStencilCollection", + getLabel: function () { + return StencilCollectionBuilder.isDocumentConfiguredAsStencilCollection() ? + "Configure Stencil Collection..." : "Configure as Stencil Collection..."; + }, + isAvailable: function () { return Pencil.controller && Pencil.controller.doc; }, + run: function () { + new StencilCollectionBuilder(Pencil.controller).configure(); + } + })); + developerToolSubItems.push(UICommandManager.register({ + key: "unconfigureStencilCollection", + label: "Unconfigure as Stencil Collection...", + isAvailable: function () { return StencilCollectionBuilder.isDocumentConfiguredAsStencilCollection(); }, + run: function () { + new StencilCollectionBuilder(Pencil.controller).removeCurrentDocumentOptions(); + } + })); + + developerToolSubItems.push(UICommandManager.register({ + key: "buildStencilCollection", + label: "Build Stencil Collection...", + shortcut: "Ctrl+B", + isAvailable: function () { return Pencil.controller && Pencil.controller.doc; }, + run: function () { + new StencilCollectionBuilder(Pencil.controller).build(); + } + })); + developerToolSubItems.push(Menu.SEPARATOR); + developerToolSubItems.push({ key: "copyAsShortcut", label: "Generate Shortcut XML...", isAvailable: function () { return Pencil.activeCanvas && Pencil.activeCanvas.currentController - && Pencil.activeCanvas.currentController.generateShortcutXML; + && Pencil.activeCanvas.currentController.generateShortcutXML && devEnable; }, run: function () { Pencil.activeCanvas.currentController.generateShortcutXML(); @@ -172,20 +209,27 @@ MainMenu.prototype.setup = function () { }); developerToolSubItems.push(Menu.SEPARATOR); - developerToolSubItems.push({ + developerToolSubItems.push(UICommandManager.register({ key: "openDeveloperTools", label: "Open Developer Tools", shortcut: "Ctrl+Alt+Shift+P", run: function () { Pencil.app.mainWindow.openDevTools(); } + })); + + toolSubItems.push({ + key: "devToolCommand", + label: "Developer Tools", + type: "SubMenu", + subItems: developerToolSubItems }); this.register({ key: "toolCommand", label: "Tools", type: "SubMenu", - subItems: developerToolSubItems + subItems: toolSubItems }); this.separator(); this.register({ @@ -198,14 +242,15 @@ MainMenu.prototype.setup = function () { } }); this.separator(); - this.register({ + this.register(UICommandManager.register({ key: "exitApplicationCommand", label: "Exit", isValid: function () { return true; }, + shortcut: "Ctrl+Q", run: function () { let remote = require("electron").remote; let currentWindow = remote.getCurrentWindow(); currentWindow.close(); } - }); + })); } diff --git a/app/views/menus/Menu.js b/app/views/menus/Menu.js index 446ef6cf..28c073b0 100644 --- a/app/views/menus/Menu.js +++ b/app/views/menus/Menu.js @@ -215,6 +215,9 @@ Menu.prototype.renderItem = function (item) { return hbox; }; +Menu.prototype.getMenuItemNodes = function () { + return this.popupContainer.childNodes; +}; Menu.prototype.render = function () { Dom.empty(this.popupContainer); var actualItems = []; diff --git a/app/views/menus/Menu.xhtml b/app/views/menus/Menu.xhtml index 6475dd91..66c84127 100644 --- a/app/views/menus/Menu.xhtml +++ b/app/views/menus/Menu.xhtml @@ -20,7 +20,8 @@ margin: 0.8em 0em; } body .MenuPopupContainer .MenuItem.Active:not([disabled='true']), - body .MenuPopupContainer .MenuItem:not([disabled='true']):hover { + body .MenuPopupContainer .MenuItem:not([disabled='true']):hover, + body .MenuPopupContainer .MenuItem[selected='true']:not([disabled='true']) { background: @selected_bg; color: @selected_fg; } diff --git a/app/views/tools/StencilCollectionBuilder.js b/app/views/tools/StencilCollectionBuilder.js new file mode 100644 index 00000000..b51ab008 --- /dev/null +++ b/app/views/tools/StencilCollectionBuilder.js @@ -0,0 +1,1396 @@ +function StencilCollectionBuilder(controller) { + this.controller = controller; +} + +StencilCollectionBuilder.COLLECTION_UTIL = ` +collection.BOUND_CALCULATOR = { + L: function (box, v) { return (box.x || 0) + v;}, + T: function (box, v) { return (box.y || 0) + v;}, + R: function (box, v) { return (box.x || 0) + box.w - v;}, + B: function (box, v) { return (box.y || 0) + box.h - v;}, + C: function (box, v) { return (box.x || 0) + box.w / 2 + v;}, + M: function (box, v) { return (box.y || 0) + box.h / 2 + v;}, + H0X: function (box, v, h0, h1) { + return h0.x + v; + }, + H0Y: function (box, v, h0, h1) { + return h0.y + v; + }, + H1X: function (box, v, h0, h1) { + return h1.x + v; + }, + H1Y: function (box, v, h0, h1) { + return h1.y + v; + }, + + calculate: function (box, spec, h0, h1) { + spec.match(/^([A-Z0-9]*[A-Z])([0-9\-]+)$/) + return collection.BOUND_CALCULATOR[RegExp.$1](box, parseInt(RegExp.$2, 10), h0, h1); + } +}; +collection.toBounds = function (box, textBounds, h0, h1) { + var literal = textBounds.value || textBounds.toString(); + var parts = literal.split(","); + var x = collection.BOUND_CALCULATOR.calculate(box, parts[0], h0, h1); + var y = collection.BOUND_CALCULATOR.calculate(box, parts[1], h0, h1); + var w = collection.BOUND_CALCULATOR.calculate(box, parts[2], h0, h1) - x; + var h = collection.BOUND_CALCULATOR.calculate(box, parts[3], h0, h1) - y; + + return new Bound(x, y, w, h); +}; +collection.calculateBoundsFromPolicy = function (box, originalInfo, policy) { + var hLayout = Group.calculateLayout(originalInfo.x0, originalInfo.w0, originalInfo.gw0, policy.xPolicy, policy.wPolicy, box.w, originalInfo.w0); + var vLayout = Group.calculateLayout(originalInfo.y0, originalInfo.h0, originalInfo.gh0, policy.yPolicy, policy.hPolicy, box.h, originalInfo.h0); + + return new Bound(Math.round(hLayout.pos), Math.round(vLayout.pos), Math.round(hLayout.size), Math.round(vLayout.size)); +}; +collection.copyClipboardImage = function (target, imageDataPropName, boxPropName) { + try { + var image = clipboard.readImage(); + if (image) { + var id = Pencil.controller.nativeImageToRefSync(image); + + var size = image.getSize(); + var newImageData = new ImageData(size.width, size.height, ImageData.idToRefString(id)); + target.setProperty(imageDataPropName, newImageData); + if (boxPropName) target.setProperty(boxPropName, new Dimension(size.width, size.height)); + } + } catch (e) { + console.error(e); + } +}; + +collection.buildNPatchModel = function (cells, originalSize, newSize) { + var totalScaleSize = 0; + for (var cell of cells) totalScaleSize += (cell.to - cell.from); + + var r = (newSize - (originalSize - totalScaleSize)) / totalScaleSize; + + var models = []; + var total = 0; + var scaledTotal = 0; + var last = false; + + //add a sentinel + cells = cells.concat([{from: originalSize, to: originalSize + 1}]); + + for (var i = 0; i < cells.length; i ++) { + var cell = cells[i]; + if (cell.from == cell.to) continue; + + var last = (i == cell.length - 2); + + var model = null; + if (cell.from > total) { + model = { + start: total, + size: cell.from - total, + scaledStart: scaledTotal, + scale: false + }; + + models.push(model); + total = cell.from; + scaledTotal += model.size; + } + + if (cell.from >= originalSize) break; + + var scaledSize = (last ? (newSize - (originalSize - cell.to) - scaledTotal) : (r * (cell.to - cell.from))); + + model = { + start: total, + size: cell.to - cell.from, + scaledStart: scaledTotal, + scaledSize: scaledSize, + scale: true + }; + + model.r = model.scaledSize / model.size; + + models.push(model); + total = cell.to; + scaledTotal += model.scaledSize; + } + + return models; +}; + +collection.parsePathData = function (pathDataLiteral) { + function normalize(pin) { + pin.x = Math.round(pin.x); + if (typeof(pin.y) == "number") pin.y = Math.round(pin.y); + } + function normalizeAll(pins) { + for (var pin of pins) normalize(pin); + } + + function processMultiPoints(points, current, chunk, relative) { + var count = Math.ceil(points.length / chunk); + for (var i = 0; i < count; i ++) { + var pin = points[i * chunk + (chunk - 1)]; + + for (var j = 0; j < (chunk - 1); j ++) { + var p = points[i * chunk + j]; + if (relative) { + p.x += current.x; + p.y += current.y; + } + + p.fixed = true; + } + + normalize(pin); + + if (relative) { + pin.x += current.x; + pin.y += current.y; + } + current = pin; + } + + return current; + } + + //parse the original data + var RE = /([A-Z])([^A-Z]*)/gi; + var commands = []; + var result = null; + var current = {x: 0, y: 0}; + while ((result = RE.exec(pathDataLiteral))) { + var c = result[1]; + var command = { + command: c.toUpperCase() + }; + var data = result[2].trim(); + if (data) { + var DATA_RE = /(\-?[0-9\.]+)(\,(\-?[0-9\.]+))?/g; + var points = []; + var result2 = null; + while ((result2 = DATA_RE.exec(data))) { + var x = parseFloat(result2[1]); + var y = result2[3]; + if (y) y = parseFloat(y); + points.push({ + x: x, + y: y + }); + } + + if (c == "M" || c == "L" || c == "T") { + normalizeAll(points); + command.points = points; + current = points[points.length - 1]; + } else if (c == "m" || c == "l" || c == "t") { + for (var p of points) { + p.x += current.x; + p.y += current.y; + + current = p; + } + normalizeAll(points); + command.points = points; + } else if (c == "H") { + for (var p of points) { + console.log("HX:", p.x); + p.y = current.y; + current = p; + } + normalizeAll(points); + command.points = points; + command.command = "L"; + } else if (c == "h") { + for (var p of points) { + p.x += current.x; + p.y = current.y; + current = p; + } + normalizeAll(points); + command.points = points; + command.command = "L"; + } else if (c == "V") { + for (var p of points) { + p.y = p.x; + p.x = current.x; + current = p; + } + normalizeAll(points); + command.points = points; + command.command = "L"; + } else if (c == "v") { + for (var p of points) { + p.y = p.x + current.y; + p.x = current.x; + current = p; + } + normalizeAll(points); + command.points = points; + command.command = "L"; + } else if (c == "c" || c == "C") { + current = processMultiPoints(points, current, 3, c == "c"); + command.points = points; + } else if (c == "s" || c == "S") { + current = processMultiPoints(points, current, 2, c == "s"); + + command.points = points; + } else if (c == "q" || c == "Q") { + current = processMultiPoints(points, current, 2, c == "q"); + command.points = points; + } else if ((c == "a" || c == "A") && points.length == 5) { + for (var p of points) { + p.fixed = true; + p.noRelativeRecalcuate = true; + console.log("p.y", p.y); + } + var pin = points[4]; + pin.fixed = false; + pin.noRelativeRecalcuate = false; + if (c == "a") { + pin.x += current.y; + pin.y += current.y; + } + current = pin; + + normalizeAll(points); + command.points = points; + command.command = "A"; + } + } + + commands.push(command); + } + + return commands; + +}; + +collection.calculateScaledPosition = function (value, models) { + if (!models || models.length == 0) return value; + var m = null; + + if (value < models[0].start) { + m = models[0]; + } else { + for (var model of models) { + if (model.start <= value && value < (model.start + model.size)) { + m = model; + break; + } + } + + if (!m) m = models[models.length - 1]; + } + + if (m) { + var d = value - m.start; + + if (m.scale) d *= m.r; + + return d + m.scaledStart; + } + + return value; +}; + + +collection.scalePathData = function (pathCommands, xCells, yCells, originalSize, newSize) { + xCells = xCells || []; + yCells = yCells || []; + + var xModel = collection.buildNPatchModel(xCells, originalSize.w, newSize.w); + var yModel = collection.buildNPatchModel(yCells, originalSize.h, newSize.h); + + var newData = ""; + + for (var command of pathCommands) { + if (command.points) { + var last = -1; + for (var i = 0; i < command.points.length; i ++) { + var pin = command.points[i]; + if (pin.fixed) { + continue; + } + + var x = collection.calculateScaledPosition(pin.x, xModel); + var y = collection.calculateScaledPosition(pin.y, yModel); + + for (var j = last + 1; j < i; j ++) { + if (command.points[j].noRelativeRecalcuate) continue; + command.points[j].x = x + command.points[j].x - pin.x; + if (typeof(command.points[j].y) == "number") command.points[j].y = y + command.points[j].y - pin.y; + } + + pin.x = x; + pin.y = y; + last = i; + } + } + + if (newData) newData += " "; + newData += command.command; + if (command.points) { + for (var i = 0; i < command.points.length; i ++) { + var y = command.points[i].y; + newData += (i > 0 ? " " : "") + command.points[i].x + (typeof(y) == "number" ? ("," + y) : ""); + } + } + } + + return newData; +}; +collection.generatePathDOM = function (svgPathData, size, keepPathStyle) { + var specs = []; + var json = svgPathData.data; + if (!json.startsWith("json:")) return specs; + var parsedPathData = JSON.parse(json.substring(5)); + + for (var info of parsedPathData) { + var d = collection.scalePathData(info.commands, svgPathData.xCells, svgPathData.yCells, svgPathData, size); + specs.push({ + _name: "path", + _uri: PencilNamespaces.svg, + d: d, + style: keepPathStyle ? info.style : "" + }); + } + + return Dom.newDOMFragment(specs); +}; +collection.generateAdvancedRectPathData = function (box, strokeStyle, r, withTop, withRight, withBottom, withLeft, withTopLeftCorner, withTopRightCorner, withBottomRightCorner, withBottomLeftCorner) { + var x = r * 4 * (Math.sqrt(2) - 1) / 3; + var w = box.w - strokeStyle.w * ((withLeft ? 0.5 : 0) + (withRight ? 0.5 : 0)); + var h = box.h - strokeStyle.w * ((withTop ? 0.5 : 0) + (withBottom ? 0.5 : 0)); + var parts = [ + ]; + var close = true; + if (withTop) { + parts.push(L(w - (withRight && withTopRightCorner ? r : 0),0)); + if (withRight && withTopRightCorner && r > 0) parts.push(c(x,0,r,r-x,r,r)); + } else { + parts.push(M(w,0)); + close = false; + } + + if (withRight) { + parts.push(L(w,h - (withBottom && withBottomRightCorner ? r : 0))); + if (withBottom && withBottomRightCorner && r > 0) parts.push(c(0,x,x-r,r,0-r,r)); + } else { + parts.push(M(w,h)); + close = false; + } + + if (withBottom) { + parts.push(L(withLeft && withBottomLeftCorner ? r : 0,h)); + if (withLeft && withBottomLeftCorner && r > 0) parts.push(c(x-r,0,0-r,x-r,0-r,0-r)); + } else { + parts.push(M(0,h)); + close = false; + } + + if (withLeft) { + parts.push(L(0,withTop && withTopLeftCorner ? r : 0)); + if (withTop && withTopLeftCorner && r > 0) parts.push(c(0,0-x,r-x,0-r,r,0-r)); + } else { + parts.push(M(0,0)); + close = false; + } + + if (close) parts.push(z); + + var firstMove = -1; + for (var i = 0; i < parts.length; i ++) { + if (parts[i].indexOf("M") == 0) { + firstMove = i; + break; + } + } + + if (firstMove > 0) { + while (firstMove > 0) { + parts.push(parts.shift()); + firstMove --; + } + } else { + parts.unshift(M(withLeft ? r : 0,0)); + } + + return parts; +}; +collection.toColorizedDOMNode = function (svgXML, color) { + if (!svgXML) return document.createDocumentFragment(); + + var svg = Dom.parseDocument(svgXML); + + if (color) { + var c = color.toRGBAString(); + Dom.workOn("//svg:*", svg, function (node) { + if (node.style.fill != "none") { + node.style.fill = c; + } + if (node.style.stroke && node.style.stroke != "none") { + node.style.stroke = c; + } + + var a = node.getAttribute("fill"); + if (a != "none") node.setAttribute("fill", c); + + a = node.getAttribute("stroke"); + if (a && a != "none") node.setAttribute("stroke", c); + }); + } + + var g = svg.createElementNS(PencilNamespaces.svg, "g"); + while (svg.documentElement.firstChild) { + var child = svg.documentElement.firstChild; + svg.documentElement.removeChild(child); + g.appendChild(child); + } + + return g; +}; +`; + +StencilCollectionBuilder.COLLECTION_RESOURCE_SCRIPT = ` +collection.browseResource = function (setNames, type, returnType, callback) { + var options = { + prefixes: [], + type: type || CollectionResourceBrowserDialog.TYPE_BITMAP, + returnType: returnType || CollectionResourceBrowserDialog.RETURN_IMAGEDATA + }; + + setNames = (setNames || "").trim(); + + for (var resource of collection.RESOURCE_LIST) { + if ((!resource.type || resource.type == options.type) && (!setNames || setNames.indexOf(resource.name) >= 0)) { + options.prefixes.push(resource); + } + } + + CollectionResourceBrowserDialog.open(collection, options, callback); +}; +`; +StencilCollectionBuilder.prototype.getPageMargin = function () { + var pageMargin = Pencil.controller.getDocumentPageMargin(); + return pageMargin || 0; +}; +StencilCollectionBuilder.prototype.toCollectionReadyImageData = function (imageData, name) { + var value = ImageData.fromString(imageData.toString()); + if (value.data && value.data.match(/^ref:\/\//)) { + var id = ImageData.refStringToId(value.data); + if (id) { + var filePath = Pencil.controller.refIdToFilePath(id); + + var bitmapImageFileName = (StencilCollectionBuilder._currentPage.name + "-" + name).replace(/[^a-z0-9\\-]+/gi, "").toLowerCase() + ".png"; + + if (!fs.existsSync(this.currentBitmapDir)) fs.mkdirSync(this.currentBitmapDir); + + var targetPath = path.join(this.currentBitmapDir, bitmapImageFileName); + fs.writeFileSync(targetPath, fs.readFileSync(filePath)); + + value = new ImageData(value.w, value.h, "collection://bitmaps/" + bitmapImageFileName, value.xCells, value.yCells); + } + } + return value; +}; +StencilCollectionBuilder.getCurrentDocumentOptions = function () { + var json = Pencil.controller.doc.properties.stencilBuilderOptions; + if (json) { + try { + return JSON.parse(json); + } catch (e) { + console.error(e); + } + } + + return null; +}; +StencilCollectionBuilder.prototype.setCurrentDocumentOptions = function (options) { + options.pageMargin = Config.get(Config.DEV_PAGE_MARGIN_SIZE); + Pencil.controller.doc.properties.stencilBuilderOptions = JSON.stringify(options); + + window.globalEventBus && window.globalEventBus.broadcast("doc-options-change", {}); +}; +StencilCollectionBuilder.prototype.removeCurrentDocumentOptions = function (options) { + if (StencilCollectionBuilder.isDocumentConfiguredAsStencilCollection()) { + Dialog.confirm( + "Are you sure you want to remove the configuration and stop using this document as a stencil collection?", null, + "Yes, remove configuration", function () { + delete Pencil.controller.doc.properties.stencilBuilderOptions; + window.globalEventBus && window.globalEventBus.broadcast("doc-options-change", {}); + }, + "Cancel", function () { + } + ); + + } +}; +StencilCollectionBuilder.prototype.makeDefaultOptions = function () { + options = options || {}; + var defaultDocName = Pencil.controller.getDocumentName().replace(/\*/g, "").trim(); + var systemUsername = os.userInfo().username; + + options.displayName = defaultDocName; + options.id = (systemUsername + "." + defaultDocName.replace(/[^a-z0-9]+/gi, "")); + options.description = ""; + options.author = systemUsername; + options.url = ""; + + options.extraScript = ""; + options.embedReferencedFonts = true; + options.resourceSets = []; + + return options; +}; +StencilCollectionBuilder.prototype.configure = function () { + var thiz = this; + var currentOptions = StencilCollectionBuilder.getCurrentDocumentOptions(); + new StencilCollectionDetailDialog().callback(function (options) { + thiz.setCurrentDocumentOptions(options); + if (Pencil.controller.documentPath) { + Pencil.documentHandler.saveDocument(); + } + }).open(currentOptions); +}; +StencilCollectionBuilder.isDocumentConfiguredAsStencilCollection = function () { + return Pencil.controller.doc && Pencil.controller.doc.properties && Pencil.controller.doc.properties.stencilBuilderOptions +}; +StencilCollectionBuilder.prototype.build = function () { + var thiz = this; + function next(options, outputPath) { + if (options) { + options.outputPath = outputPath; + thiz.setCurrentDocumentOptions(options); + thiz.buildImpl(options); + } else { + new StencilCollectionDetailDialog("Build").callback(function (options) { + options.outputPath = outputPath; + thiz.setCurrentDocumentOptions(options); + thiz.buildImpl(options); + }).open(null); + } + } + + var currentOptions = StencilCollectionBuilder.getCurrentDocumentOptions(); + + if (!currentOptions + || !currentOptions.outputPath + || !fs.existsSync(currentOptions.outputPath) + || Pencil.controller.doc._lastUsedStencilOutputPath != currentOptions.outputPath) { + dialog.showOpenDialog(remote.getCurrentWindow(), { + title: "Select Output Directory", + defaultPath: (currentOptions && currentOptions.outputPath && fs.existsSync(currentOptions.outputPath)) ? currentOptions.outputPath : os.homedir(), + properties: ["openDirectory"] + }, function (filenames) { + if (!filenames || filenames.length <= 0) return; + var selectedPath = filenames[0]; + + next(currentOptions, selectedPath); + }); + + } else { + next(currentOptions, currentOptions.outputPath); + } +}; +StencilCollectionBuilder.prototype.buildImpl = function (options) { + if (!options) { + options = this.makeDefaultOptions(); + } + StencilCollectionBuilder.INSTANCE = this; + var dir = options.outputPath; + this.iconDir = path.join(dir, "icons"); + + var thiz = this; + + this.currentDir = dir; + this.currentBitmapDir = path.join(dir, "bitmaps"); + + if (!fs.existsSync(this.iconDir)) { + fs.mkdirSync(this.iconDir); + } + + var dom = Controller.parser.parseFromString("", "text/xml"); + var shapes = dom.documentElement; + + shapes.setAttribute("id", options.id); + shapes.setAttribute("displayName", options.displayName); + shapes.setAttribute("author", options.author); + shapes.setAttribute("description", options.description); + shapes.setAttribute("url", options.url); + + shapes.appendChild(Dom.newDOMElement({ + _name: "Script", + _uri: PencilNamespaces.p, + comments: "Built-in util script", + _cdata: StencilCollectionBuilder.COLLECTION_UTIL + })); + + if (options.extraScript) { + shapes.appendChild(Dom.newDOMElement({ + _name: "Script", + _uri: PencilNamespaces.p, + comments: "Extra script", + _cdata: "\n" + options.extraScript + "\n" + })); + } + + var resourceList = []; + + //processing resources + if (options.resourceSets) { + var base = path.dirname(Pencil.controller.documentPath); + + var resourceDirName = "resources"; + var resourceDir = path.join(dir, resourceDirName); + + if (!fsExistSync(resourceDir)) { + fs.mkdirSync(resourceDir); + } + + for (var set of options.resourceSets) { + var sourcePath = base ? path.resolve(base, set.path) : set.path; + if (!fs.existsSync(sourcePath)) { + console.error("Resource dir not found: " + sourcePath); + continue; + } + + var name = set.name.replace(/[^a-z0-9]+/gi, "_"); + var destPath = path.join(resourceDir, name); + resourceList.push({ + name: name, + prefix: resourceDirName + "/" + name + }); + + if (!fs.existsSync(destPath)) { + fs.mkdirSync(destPath); + var files = fs.readdirSync(sourcePath); + files.forEach(function (file) { + var curSource = path.join(sourcePath, file); + if (fs.lstatSync(curSource).isDirectory()) { + copyFolderRecursiveSync(curSource, destPath); + } else { + copyFileSync(curSource, destPath); + } + }); + } + } + + var script = "collection.RESOURCE_LIST = " + JSON.stringify(resourceList) + ";\n" + StencilCollectionBuilder.COLLECTION_RESOURCE_SCRIPT; + shapes.appendChild(Dom.newDOMElement({ + _name: "Script", + _uri: PencilNamespaces.p, + comments: "Resource script", + _cdata: "\n" + script+ "\n" + })); + } + + var globalPropertySpecs = []; + + shapes.appendChild(Dom.newDOMElement({ + _name: "Properties", + _uri: PencilNamespaces.p, + _children: [ + { + _name: "PropertyGroup", + _uri: PencilNamespaces.p, + name: "Collection Properties", + _children: [] + } + ] + })); + + var layoutPage = null; + + var pageMargin = this.getPageMargin(); + var ts = new Date().getTime(); + + ApplicationPane._instance.setContentVisible(false); + + var finalize = function () { + this.saveResultDom(dom, dir, options, function () { + var showDone = function () { + Pencil.controller.doc._lastUsedStencilOutputPath = options.outputPath; + + thiz.progressListener.onTaskDone(); + ApplicationPane._instance.setContentVisible(true); + + var stencilPath = Config.get("dev.stencil.path", null); + var dirPath = Config.get("dev.stencil.dir", null); + if (stencilPath == options.outputPath || dirPath == path.dirname(options.outputPath)) { + CollectionManager.reloadDeveloperStencil(false); + NotificationPopup.show("Stencil collection '" + options.displayName + "' was successfully built.\n\nDeveloper stencil was also reloaded.", "View", function () { + shell.openItem(options.outputPath); + }); + } else { + NotificationPopup.show("Stencil collection '" + options.displayName + "' was successfully built.", "View", function () { + shell.openItem(options.outputPath); + }); + } + + }; + + if (layoutPage) { + thiz.generateCollectionLayout(options.id, dir, layoutPage, showDone); + } else { + showDone(); + } + + }); + }.bind(this); //END OF FINAL PROCESSING + + var nonStencilPages = []; + + var done = function () { + var globalPropertyMap = {}; + + //append global propert fragment + if (globalPropertySpecs && globalPropertySpecs.length > 0) { + var globalGroupNode = Dom.getSingle("/p:Shapes/p:Properties/p:PropertyGroup", dom); + globalGroupNode.appendChild(Dom.newDOMFragment(globalPropertySpecs, dom)); + + for (var spec of globalPropertySpecs) { + var prop = spec._prop; + globalPropertyMap[prop.name] = prop.type.fromString(prop.value); + } + } + + //re-fill shape's property fragment + Dom.workOn("/p:Shapes/p:Shape", dom, function (shapeDefNode) { + if (shapeDefNode._propertyFragmentSpec && shapeDefNode._propertyFragmentSpec.length > 0) { + //generalizing global properties + for (var spec of shapeDefNode._propertyFragmentSpec) { + var prop = spec._prop; + var value = prop.type.fromString(prop.value); + if (!value) continue; + + var previousDiff = null; + + for (var globalSpec of globalPropertySpecs) { + if (spec.type != globalSpec.type) continue; + + var globalName = globalSpec._prop.name; + var globalPropValue = globalPropertyMap[globalName]; + if (!globalPropValue || !globalPropValue.generateTransformTo) continue; + + var transformSpec = globalPropValue.generateTransformTo(value); + var diff = globalPropValue.getDiff ? globalPropValue.getDiff(value) : ((transformSpec === "" || transformSpec) ? transformSpec.length : 10000000); + if ((transformSpec === "" || transformSpec) && (previousDiff === null || previousDiff > diff)) { + delete spec._text; + spec._children = [{ + _name: "E", + _uri: PencilNamespaces.p, + _text: "$$" + globalName + transformSpec + }]; + + previousDiff = diff; + } + } + } + + var groupNode = Dom.getSingle("./p:Properties/p:PropertyGroup[@holder='true']", shapeDefNode); + groupNode.appendChild(Dom.newDOMFragment(shapeDefNode._propertyFragmentSpec, dom)); + } + }); + + this.processShortcuts(nonStencilPages, dom, dir, options, globalPropertySpecs, globalPropertyMap, function () { + finalize(); + }); + }.bind(this); + + this.progressListener = null; + + var index = -1; + var pages = this.controller.doc.pages; + var next = function() { + index ++; + if (index >= pages.length) { + done(); + return; + } + + var page = pages[index]; + + this.progressListener.onProgressUpdated(`Exporting '${page.name}...'`, index, pages.length); + + try { + if (page.name.toLowerCase() == "(layout)") { + layoutPage = page; + return; + } + + ApplicationPane._instance.activatePage(page); + var svg = page.canvas.svg; + StencilCollectionBuilder._currentPage = page; + + var properties = []; + var behaviors = []; + var actions = []; + var shapeId = page.name.replace(/[^a-z0-9\-]+/gi, "").toLowerCase(); + var shapeSpec = { + _name: "Shape", + _uri: PencilNamespaces.p, + id: shapeId, + displayName: page.name, + icon: "icons/" + shapeId + ".png?token=" + ts, + _children: [ + { + _name: "Properties", + _uri: PencilNamespaces.p, + _children: [ + { + _name: "PropertyGroup", + _uri: PencilNamespaces.p, + name: "Common", + holder: "true", + _children: [] //leave this blank, actual property definitions will be filled later + } + ] + }, + { + _name: "Behaviors", + _uri: PencilNamespaces.p, + _children: behaviors + }, + { + _name: "Actions", + _uri: PencilNamespaces.p, + _children: actions + } + ] + }; + + if (page.note) shapeSpec.description = Dom.htmlStrip(page.note); + + var propertyMap = {}; + + var contentNode = Dom.newDOMElement({ + _name: "Content", + _uri: PencilNamespaces.p + }, dom); + + var contentFragment = dom.createDocumentFragment(); + var clipPathFragment = dom.createDocumentFragment(); + + var snaps = []; + + var defs = null; + var clipPathNodeMap = {}; + + var hasContribution = false; + + Dom.workOn(".//svg:g[@p:type='Shape']", svg, function (shapeNode) { + var c = page.canvas.createControllerFor(shapeNode); + + if (!c.performAction) return; + var contribution = null; + try { + contribution = c.performAction("buildShapeContribution"); + } catch (e) { + console.error("Error in building shape contribution:", e); + return; + } + + if (!contribution) return; + + for (var prop of contribution.properties) { + if (!prop.global) { + if (propertyMap[prop.name]) continue; + if (prop.name == "box") { + prop.value = new Dimension(page.width - 2 * pageMargin, page.height - 2 * pageMargin).toString(); + } + } + var node = { + _name: "Property", + _uri: PencilNamespaces.p, + name: prop.name, + displayName: prop.displayName, + type: prop.type.name, + _text: prop.value, + _prop: prop + }; + if (prop.meta) { + for (var metaName in prop.meta) { + node["p:" + metaName] = prop.meta[metaName]; + } + } + + if (prop.global) { + globalPropertySpecs.push(node); + } else { + propertyMap[prop.name] = node; + properties.push(node); + } + } + + for (var targetName in contribution.behaviorMap) { + var set = contribution.behaviorMap[targetName]; + var behaviorSpec = { + _name: "For", + _uri: PencilNamespaces.p, + ref: set.ref, + _children: [] + } + for (var item of set.items) { + var itemSpec = { + _name: item.behavior, + _uri: PencilNamespaces.p, + _children: [] + } + for (var arg of item.args) { + itemSpec._children.push({ + _name: "Arg", + _uri: PencilNamespaces.p, + _text: arg + }) + } + behaviorSpec._children.push(itemSpec); + } + + behaviors.push(behaviorSpec); + } + + for (var action of contribution.actions) { + var node = { + _name: "Action", + _uri: PencilNamespaces.p, + id: action.id, + displayName: action.displayName, + _children: [ + { + _name: "Impl", + _uri: PencilNamespaces.p, + _cdata: action.impl + } + ] + }; + if (action.meta) { + for (var metaName in action.meta) { + node["p:" + metaName] = action.meta[metaName]; + } + } + + actions.push(node); + } + + if (contribution.snaps) { + for (var snap of contribution.snaps) { + snap._contribution = contribution; + snaps.push(snap); + } + } + + if (contribution.contentFragment && contribution.contentFragment.childNodes.length > 0) { + if (contribution.clipPathName) { + var clipPathNode = clipPathNodeMap[contribution.clipPathName]; + if (!clipPathNode) { + if (!defs) { + defs = contentNode.ownerDocument.createElementNS(PencilNamespaces.svg, "defs"); + contentNode.appendChild(defs); + } + + clipPathNode = contentNode.ownerDocument.createElementNS(PencilNamespaces.svg, "clipPath"); + clipPathNode.setAttribute("id", contribution.clipPathName); + defs.appendChild(clipPathNode); + clipPathNodeMap[contribution.clipPathName] = clipPathNode; + } + + clipPathNode.appendChild(contribution.contentFragment); + } else { + var e = contribution.contentFragment; + if (contribution.clippedByName) { + var g = contentNode.ownerDocument.createElementNS(PencilNamespaces.svg, "g"); + g.appendChild(e); + g.setAttribute("style", "clip-path: url(#" + contribution.clippedByName + ");"); + + e = g; + } + contentFragment.appendChild(e); + } + } + + hasContribution = true; + }); + + if (!hasContribution) { + nonStencilPages.push(page); + return; + } + + contentNode.appendChild(contentFragment); + + // if the shape has 'box', add standard snaps + if (propertyMap["box"]) { + snaps = [ + {name: "Left", accept: "Left", horizontal: true, expression: "0"}, + {name: "Right", accept: "Right", horizontal: true, expression: "$box.w"}, + {name: "Top", accept: "Top", horizontal: false, expression: "0"}, + {name: "Bottom", accept: "Bottom", horizontal: false, expression: "$box.h"}, + {name: "VCenter", accept: "VCenter", horizontal: true, expression: "Math.round($box.w / 2)"}, + {name: "HCenter", accept: "HCenter", horizontal: false, expression: "Math.round($box.h / 2)"}, + ].concat(snaps); + } + + if (snaps.length > 0) { + var impl = ""; + var header = ""; + var definedProp = {}; + var definedBound = {}; + function replaceReference(expression, snap) { + var expression = expression.replace(/\$([a-z][a-z0-9]*)/gi, function (zero, one) { + var name = "__prop_" + one; + if (!definedProp[one]) { + header += "var " + name + " = this.getProperty(\"" + one + "\");\n"; + definedProp[one] = true; + } + + return name; + }); + expression = expression.replace(/this\.def\.collection/g, "@@@"); + expression = expression.replace(/collection\./g, "this.def.collection."); + expression = expression.replace(/@@@/g, "this.def.collection"); + + if (expression.indexOf("@boundExpressionLiteral") >= 0) { + var boundVarName = "__bound_" + snap._contribution.targetElementName; + if (!definedBound[boundVarName]) { + header += "var " + boundVarName + " = " + replaceReference(snap._contribution._boundExpressionLiteral, snap) + ";\n"; + definedBound[boundVarName] = true; + } + expression = expression.replace(/@boundExpressionLiteral/g, boundVarName); + } + + + return expression; + } + for (var snap of snaps) { + var expression = replaceReference(snap.expression, snap); + + if (impl) impl += ",\n"; + impl += "new SnappingData(" + JSON.stringify(snap.name) + ", " + expression + ", " + JSON.stringify(snap.accept) + ", " + snap.horizontal + ", this.id).makeLocal(true)"; + } + + impl = header + "\nvar snaps = [" + impl + "];\nreturn snaps;"; + + var snapActionNode = { + _name: "Action", + _uri: PencilNamespaces.p, + id: "getSnappingGuide", + _children: [ + { + _name: "Impl", + _uri: PencilNamespaces.p, + _cdata: "\n" + impl + "\n" + } + ] + }; + + actions.push(snapActionNode); + } + + var shape = Dom.newDOMElement(shapeSpec, dom); + if (!contentNode.hasChildNodes()) return; + shape.appendChild(contentNode); + shapes.appendChild(shape); + + if (fs.existsSync(page.thumbPath)) { + var thumPath = path.join(thiz.iconDir, shapeId + ".png"); + fs.createReadStream(page.thumbPath).pipe(fs.createWriteStream(thumPath)); + } + + shape._propertyFragmentSpec = properties; + } finally { + window.setTimeout(next, 10); + } + }.bind(this); //END OF PAGE PROCESSING + + Util.beginProgressJob("Building collection...", function (listener) { + thiz.progressListener = listener; + next(); + }); +}; + +StencilCollectionBuilder.prototype.saveResultDom = function (dom, dir, options, callback) { + var xsltDOM = Dom.parseDocument( +` + + + + + + + +` + ); + var xsltProcessor = new XSLTProcessor(); + xsltProcessor.importStylesheet(xsltDOM); + + var result = xsltProcessor.transformToDocument(dom); + + Dom.serializeNodeToFile(result, path.join(dir, "Definition.xml")); + + if (callback) callback(); +}; + +StencilCollectionBuilder.prototype.isShortcutPage = function (page, options) { + if (options.shortcutPageIds) { + return options.shortcutPageIds.indexOf(page.id) >= 0; + } else { + return page.name.toLowerCase().startsWith("(shortcut"); + } +}; + +StencilCollectionBuilder.prototype.processShortcuts = function (pages, dom, dir, options, globalPropertySpecs, globalPropertyMap, callback) { + var thiz = this; + this.saveResultDom(dom, dir, options, function () { + + //parse the resulted collection + var collection = new ShapeDefCollectionParser().parseURL(path.join(dir, "Definition.xml")); + var shortcutSpecs = []; + + Util.workOnListAsync(pages, function(page, index, __callback) { + thiz.progressListener.onProgressUpdated(`Processing shortcuts in '${page.name}...'`, index, pages.length); + ApplicationPane._instance.activatePage(page); + var svg = page.canvas.svg; + + var defIdPrefix = collection.id + ":"; + var shapeNodes = Dom.getList(".//svg:g[@p:type='Shape']", svg); + + Util.workOnListAsync(shapeNodes, function (shapeNode, index, __callback) { + thiz.progressListener.onProgressUpdated(`Processing shortcuts in '${page.name}...'`, index, shapeNodes.length); + var defId = page.canvas.getType(shapeNode); + var def = null; + var id = null; + var effectiveCollection = null; + + if (!defId) { + __callback(); + return; + } + + if (defId.startsWith(defIdPrefix)) { + effectiveCollection = collection + id = defId.substring(defIdPrefix.length); + def = collection.shapeDefMap[defId]; + } else { + if (options.allowExternalShortcuts || (typeof(options.allowExternalShortcuts) === "undefined")) { + var def = CollectionManager.shapeDefinition.locateDefinition(defId); + id = defId; + effectiveCollection = def.collection; + } else { + __callback(); + return; + } + } + + if (!def) { + __callback(); + return; + } + + var shape = new Shape(page.canvas, shapeNode, def); + var symbolName = shape.getSymbolName(); + if (!symbolName) { + __callback(); + return; + } + + var spec = { + _name: "Shortcut", + _uri: PencilNamespaces.p, + to: id, + displayName: symbolName, + _children: [ + + ] + }; + + for (var name in def.propertyMap) { + var prop = def.getProperty(name); + var value = shape.getProperty(name); + + if (prop.type == ImageData) { + if (value.data && value.data.match(/^ref:\/\//)) { + if (prop.initialValue && prop.initialValue.data && prop.initialValue.data.match(/^collection:\/\/(.+)$/)) { + var declaredPath = RegExp.$1; + var ref = ImageData.idToRefString(thiz.controller.generateCollectionResourceRefId(effectiveCollection, declaredPath)); + if (ref == value.data) continue; + } + value = thiz.toCollectionReadyImageData(value, spec.displayName + "-" + prop.name); + } + } + + if (!value) continue; + if (prop.initialValueExpression) { + shape._evalContext = {collection: def.collection}; + var v = shape.evalExpression(prop.initialValueExpression); + if (v && value.toString() == v.toString()) continue; + } + if (prop.initialValue) { + if (value.toString() == prop.initialValue.toString()) continue; + } + + var sp = { + _name: "PropertyValue", + _uri: PencilNamespaces.p, + name: name, + _text: value.toString() + }; + + var previousDiff = null; + + for (var globalSpec of globalPropertySpecs) { + if (prop.type.name != globalSpec.type) continue; + + var globalName = globalSpec._prop.name; + + var globalPropValue = globalPropertyMap[globalName]; + if (!globalPropValue || !globalPropValue.generateTransformTo) continue; + + var transformSpec = globalPropValue.generateTransformTo(value); + + var diff = globalPropValue.getDiff ? globalPropValue.getDiff(value) : ((transformSpec === "" || transformSpec) ? transformSpec.length : 10000000); + if ((transformSpec === "" || transformSpec) && (previousDiff === null || previousDiff > diff)) { + delete sp._text; + sp._children = [{ + _name: "E", + _uri: PencilNamespaces.p, + _text: "$$" + globalName + transformSpec + }]; + + previousDiff = diff; + } + } + + spec._children.push(sp); + } + + var fileName = spec.displayName.replace(/[^a-z0-9\\-]+/gi, "").toLowerCase() + ".png"; + var targetPath = path.join(thiz.iconDir, fileName); + Pencil.rasterizer.rasterizeSelectionToFile(shape, targetPath, function (p, error) { + if (!error) { + spec.icon = "icons/" + fileName; + } + + shortcutSpecs.push(spec); + __callback(); + }); + }, __callback); + }, function () { + var fragment = Dom.newDOMFragment(shortcutSpecs, dom); + dom.documentElement.appendChild(fragment); + if (callback) callback(); + }); + }); +}; +StencilCollectionBuilder.prototype.generateCollectionLayout = function (collectionId, dir, page, callback) { + ApplicationPane._instance.activatePage(page); + var container = page.canvas.drawingLayer; + var pageMargin = StencilCollectionBuilder.INSTANCE.getPageMargin(); + + var pw = parseFloat(page.width) - 2 * pageMargin; + var ph = parseFloat(page.height) - 2 * pageMargin; + + var items = []; + + const IMAGE_FILE = "layout_image.png"; + + Dom.workOn(".//svg:g[@p:type='Shape']", container, function (g) { + var dx = 0; //rect.left; + var dy = 0; //rect.top; + + var owner = g.ownerSVGElement; + + if (owner.parentNode && owner.parentNode.getBoundingClientRect) { + var rect = owner.parentNode.getBoundingClientRect(); + dx = rect.left; + dy = rect.top; + } + + dx += pageMargin; + dy += pageMargin; + + rect = g.getBoundingClientRect(); + + var linkingInfo = { + node: g, + sc: g.getAttributeNS(PencilNamespaces.p, "sc"), + refId: g.getAttributeNS(PencilNamespaces.p, "def"), + geo: { + x: rect.left - dx, + y: rect.top - dy, + w: rect.width - 2, + h: rect.height - 2 + } + }; + + items.push(linkingInfo); + }); + + var current = 0; + var thiz = this; + var done = function () { + var html = document.createElementNS(PencilNamespaces.html, "html"); + + var body = document.createElementNS(PencilNamespaces.html, "body"); + html.appendChild(body); + + var div = document.createElementNS(PencilNamespaces.html, "div"); + div.setAttribute("style", "position: relative; padding: 0px; margin: 0px; width: " + pw + "px; height: " + ph + "px;"); + body.appendChild(div); + + var bg = document.createElementNS(PencilNamespaces.html, "img"); + bg.setAttribute("style", "width: " + pw + "px; height: " + ph + "px;"); + bg.setAttribute("src", IMAGE_FILE + "?ts=" + (new Date().getTime())); + div.appendChild(bg); + + for (var i = 0; i < items.length; i ++) { + var link = items[i]; + var img = document.createElementNS(PencilNamespaces.html, "img"); + img.setAttribute("src", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII="); + if (link.sc) { + img.setAttribute("sc-ref", link.sc); + } else { + img.setAttribute("ref", link.refId); + } + img.setAttribute("id", link.refId); + var css = new CSS(); + css.set("position", "absolute"); + css.set("left", "" + link.geo.x + "px"); + css.set("top", "" + link.geo.y + "px"); + css.set("width", "" + link.geo.w + "px"); + css.set("height", "" + link.geo.h + "px"); + img.setAttribute("style", css.toString()); + + div.appendChild(img); + } + + Dom.serializeNodeToFile(html, path.join(dir, "Layout.xhtml"), ""); + if (callback) callback(); + }; + + + var outputImage = path.join(dir, IMAGE_FILE); + Pencil.rasterizer.rasterizePageToFile(page, outputImage, function (p, error) { + done(); + }); +}; +StencilCollectionBuilder.prototype.generatePrivateShapeDef = function (target, callback) { + var svg = target.svg.cloneNode(true); + + var fakeDom = Controller.parser.parseFromString("", "text/xml"); + fakeDom.documentElement.appendChild(svg); + + Pencil.controller.prepareForEmbedding(fakeDom, function () { + fakeDom.documentElement.removeChild(svg); + + var shapeDef = new PrivateShapeDef(); + shapeDef.displayName = target.getSymbolName(); + shapeDef.content = svg; + shapeDef.id = ("PrivateShapeDef_" + shapeDef.displayName).replace(/\s+/g, "_").toLowerCase(); + + Util.generateIcon(target, 64, 64, 2, null, function (icondata) { + shapeDef.iconData = icondata; + callback(shapeDef); + }); + }); +}; diff --git a/app/views/tools/StencilCollectionDetailDialog.js b/app/views/tools/StencilCollectionDetailDialog.js new file mode 100644 index 00000000..063f97e0 --- /dev/null +++ b/app/views/tools/StencilCollectionDetailDialog.js @@ -0,0 +1,112 @@ +function StencilCollectionDetailDialog(acceptActionLabel) { + Dialog.call(this); + this.title = "Stencil Collection Details"; + this.acceptActionLabel = acceptActionLabel; + + this.bind("click", this.handleSetBrowseEvent, this.setContainer); +} +__extend(Dialog, StencilCollectionDetailDialog); + +StencilCollectionDetailDialog.prototype.handleSetBrowseEvent = function (event) { + var targetInputName = event.target ? event.target.getAttribute("target") : null; + if (!targetInputName) return; + + var input = this[targetInputName]; + var nameInput = this[targetInputName.replace(/PathInput/, "NameInput")]; + + var defaultPath = Pencil.controller.documentPath ? path.dirname(Pencil.controller.documentPath) : os.homedir(); + dialog.showOpenDialog(remote.getCurrentWindow(), { + title: "Select Resource Directory", + defaultPath: defaultPath, + properties: ["openDirectory"] + }, function (filenames) { + if (!filenames || filenames.length <= 0) return; + var selectedPath = filenames[0]; + if (Pencil.controller.documentPath) { + var base = path.dirname(Pencil.controller.documentPath); + var relative = path.relative(base, selectedPath); + const LIMIT = ".." + path.sep + ".." + path.sep + ".."; // 3 levels up means bad + if (!relative.startsWith(LIMIT)) selectedPath = relative; + } + + input.value = selectedPath; + if (!nameInput.value) { + nameInput.value = path.basename(filenames[0]); + } + }); + +}; +StencilCollectionDetailDialog.prototype.setup = function (options) { + options = options || {}; + this.originalOptions = options; + this.doc = options.doc || Pencil.controller.doc; + + var defaultDocName = Pencil.controller.getDocumentName().replace(/\*/g, "").trim(); + var systemUsername = os.userInfo().username; + + this.displayNameInput.value = options.displayName || defaultDocName; + this.idInput.value = options.id || (systemUsername + "." + defaultDocName.replace(/[^a-z0-9]+/gi, "")); + this.descriptionInput.value = options.description || ""; + this.authorNameInput.value = options.author || systemUsername; + this.urlInput.value = options.url || ""; + + this.scriptInput.value = options.extraScript || ""; + + this.embedReferencedFontsCheckbox.checked = typeof(options.embedReferencedFonts) != "boolean" ? true : options.embedReferencedFonts; + + var resourceSets = options.resourceSets || []; + + var index = 0; + for (var set of resourceSets) { + if (index > 4) break; + if (set && set.name && set.path) { + this["resourceSet" + index + "NameInput"].value = set.name.trim(); + this["resourceSet" + index + "PathInput"].value = set.path; + + index ++; + } + } +}; +StencilCollectionDetailDialog.prototype.save = function () { + var options = {}; + + if (this.originalOptions) { + for (var name in this.originalOptions) options[name] = this.originalOptions[name]; + } + + try { + + options.displayName = getRequiredValue(this.displayNameInput, "Please enter collection's name."); + options.id = getRequiredValue(this.idInput, "Please enter collection's id."); + options.description = this.descriptionInput.value; + options.author = getRequiredValue(this.authorNameInput, "Please enter author's name."); + options.url = getRequiredValue(this.urlInput, "Please enter a valid URL.", /^(http(s?):\/\/.+)?$/); + + options.extraScript = this.scriptInput.value; + options.embedReferencedFonts = this.embedReferencedFontsCheckbox.checked; + + options.resourceSets = []; + for (var i = 0; i < 5; i ++) { + var setName = this["resourceSet" + i + "NameInput"].value; + var setPath = this["resourceSet" + i + "PathInput"].value; + if (setName && setPath) { + options.resourceSets.push({ + name: setName.trim(), + path: setPath + }); + } + }; + + this.close(options); + } catch (e) { + handleCommonValidationError(e); + return false; + } +}; + +StencilCollectionDetailDialog.prototype.getDialogActions = function () { + return [ + { type: "accept", title: this.acceptActionLabel || "Save", run: function () { return this.save(); } }, + Dialog.ACTION_CANCEL + ]; +}; diff --git a/app/views/tools/StencilCollectionDetailDialog.xhtml b/app/views/tools/StencilCollectionDetailDialog.xhtml new file mode 100644 index 00000000..02e489d7 --- /dev/null +++ b/app/views/tools/StencilCollectionDetailDialog.xhtml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +