diff --git a/README.md b/README.md index ca26ed5..18a5103 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ An UML Class explorer for InterSystems Caché. + Build diagrams for any package or subpackage; + Edit diagrams after build; + Export diagrams as an image; ++ View class methods code; + Zoom in and out, explore big packages and more. ## Screenshots -![Demo](https://cloud.githubusercontent.com/assets/4989256/7586381/19008d24-f8b5-11e4-8893-a63d5373dfa1.png) +![Demo](https://cloud.githubusercontent.com/assets/4989256/7622499/9cb98048-f9d8-11e4-9c27-4257e53ec70d.png) ## Installation diff --git a/cache/projectTemplate.xml b/cache/projectTemplate.xml index 1855c46..3ac3620 100644 --- a/cache/projectTemplate.xml +++ b/cache/projectTemplate.xml @@ -3,7 +3,7 @@ Class contains methods that return structured classes/packages data. -63684,52343.305666 +63686,4398.893381 63653,67019.989197 @@ -70,18 +70,19 @@ Return structured data about class. set oClass.FINAL = classDefinition.Final set oClass.HIDDEN = classDefinition.Hidden set oClass.classType = classDefinition.ClassType + set oClass.serverOnly = classDefinition.ServerOnly // - if (oData.restrictPackage) && ('..inPackage(oData.basePackageName, package)) quit oClass set oClass.properties = oProperties set count = classDefinition.Properties.Count() - for i = 1:1:count { + for i=1:1:count { set oProp = ##class(%ZEN.proxyObject).%New() set p = classDefinition.Properties.GetAt(i) do oProperties.%DispatchSetProperty(p.Name, oProp) - do oProp.%DispatchSetProperty("private", p.Private) - do oProp.%DispatchSetProperty("readOnly", p.ReadOnly) - do oProp.%DispatchSetProperty("type", p.Type) + set oProp.private = p.Private + set oProp.readOnly = p.ReadOnly + set oProp.type = p.Type do ..collectAggregation(oData, classDefinition, p.Type, p.Private) do ..collectAggregation(oData, classDefinition, package _ "." _ p.Type, p.Private) } @@ -89,19 +90,29 @@ Return structured data about class. set oMethods = ##class(%ZEN.proxyObject).%New() set oClass.methods = oMethods set count = classDefinition.Methods.Count() - for i = 1:1:count { + for i=1:1:count { set oMeth = ##class(%ZEN.proxyObject).%New() set met = classDefinition.Methods.GetAt(i) do oMethods.%DispatchSetProperty(met.Name, oMeth) - do oMeth.%DispatchSetProperty("private", met.Private) - do oMeth.%DispatchSetProperty("returns", met.ReturnType) - do oMeth.%DispatchSetProperty("classMethod", met.ClassMethod) + set oMeth.private = met.Private + set oMeth.returns = met.ReturnType + set oMeth.classMethod = met.ClassMethod + set oMeth.clientMethod = met.ClientMethod + set oMeth.final = met.Final + set oMeth.abstract = met.Abstract + set oMeth.language = met.Language + set oMeth.notInheritable = met.NotInheritable + set oMeth.serverOnly = met.ServerOnly + set oMeth.sqlProc = met.SqlProc + set oMeth.sqlName = met.SqlName + set oMeth.webMethod = met.WebMethod + set oMeth.zenMethod = met.ZenMethod } set oParameters = ##class(%ZEN.proxyObject).%New() set oClass.parameters = oParameters set count = classDefinition.Parameters.Count() - for i = 1:1:count { + for i=1:1:count { set oPar = ##class(%ZEN.proxyObject).%New() set p = classDefinition.Parameters.GetAt(i) do oParameters.%DispatchSetProperty(p.Name, oPar) @@ -114,6 +125,30 @@ Return structured data about class. ]]> + + +Return method data. +1 +className:%String,methodName:%String +%ZEN.proxyObject + + + packageName is in basePackageName.]]> @@ -280,7 +315,7 @@ Returns structured package data REST interface for UMLExplorer %CSP.REST -63679,81701.423669 +63685,85586.177035 63648,30450.187229 @@ -294,6 +329,7 @@ REST interface for UMLExplorer + ]]> @@ -335,6 +371,19 @@ Returns all package class trees by given package name ]]> + + +Returns method description and code +1 +className:%String,methodName:%String +%Status + + + Method returns user application CSS. diff --git a/package.json b/package.json index 7ef9eec..426eb4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CacheUMLExplorer", - "version": "0.7.0", + "version": "0.8.0", "description": "An UML Class explorer for InterSystems Caché", "directories": { "test": "test" diff --git a/web/css/classView.css b/web/css/classView.css index 959105d..700c6d7 100644 --- a/web/css/classView.css +++ b/web/css/classView.css @@ -4,6 +4,13 @@ svg { } #classView { + position: relative; + height: 100%; +} + +#svgContainer { + z-index: 0; + position: relative; height: 100%; cursor: -webkit-grab; cursor: -moz-grab; } @@ -37,6 +44,10 @@ text { .uml-class-name-text { cursor: help; + -webkit-transition: all .2s ease; + -moz-transition: all .2s ease; + -o-transition: all .2s ease; + transition: all .2s ease; } .uml-class-name-text:hover { @@ -59,4 +70,16 @@ text { font-family: monospace; font-weight: 900; fill: magenta; +} + +.line-clickable { + cursor: pointer; + -webkit-transition: all .2s ease; + -moz-transition: all .2s ease; + -o-transition: all .2s ease; + transition: all .2s ease; +} + +.line-clickable:hover { + fill: red; } \ No newline at end of file diff --git a/web/css/extras.css b/web/css/extras.css index 8d33901..fc04ddf 100644 --- a/web/css/extras.css +++ b/web/css/extras.css @@ -106,6 +106,30 @@ left: 5px; } +.icon.cross:before { + content: ""; + background-color: #fff; + width: 4px; + height: 14px; + border-radius: 1px; + position: absolute; + top: 5px; + left: 10px; + transform: rotate(45deg); +} + +.icon.cross:after { + content: ""; + background-color: #fff; + width: 14px; + height: 4px; + border-radius: 1px; + position: absolute; + top: 10px; + left: 5px; + transform: rotate(45deg); +} + .icon.minus:after { content: ""; background-color: #fff; diff --git a/web/css/interface.css b/web/css/interface.css index 7e390b9..77ec99c 100644 --- a/web/css/interface.css +++ b/web/css/interface.css @@ -15,13 +15,13 @@ html, body { .ui-sideBlock { position: relative; float: left; - width: 300px; + width: 250px; height: 100%; } .ui-mainBlock { position: relative; - margin-left: 300px; + margin-left: 250px; height: 100%; } diff --git a/web/css/methodCodeView.css b/web/css/methodCodeView.css new file mode 100644 index 0000000..894636e --- /dev/null +++ b/web/css/methodCodeView.css @@ -0,0 +1,44 @@ +#methodCodeView { + position: absolute; + width: 100%; + bottom: 0; + height: 0; + background: rgba(245, 245, 245, 0.95); + z-index: 10; + -webkit-transition: all .5s ease; + -moz-transition: all .5s ease; + -o-transition: all .5s ease; + transition: all .5s ease; +} + +#methodCodeView > div.head { + position: relative; + overflow: visible; /* kept for button shadow */ + margin: 1em; +} + +#methodDescription { + box-sizing: border-box; + padding: 1em; + color: gray; + font-style: italic; +} + +#methodViewBounds { + overflow: auto; +} + +#methodCode { + padding: 1em; + box-sizing: border-box; + white-space: pre; +} + +#closeMethodCodeView { + float: right; +} + +#methodCodeView.active { + box-shadow: 0 0 5px black; + height: 100%; +} \ No newline at end of file diff --git a/web/index.html b/web/index.html index f296930..73d877e 100644 --- a/web/index.html +++ b/web/index.html @@ -9,6 +9,7 @@ + @@ -23,7 +24,7 @@ - +
@@ -46,7 +47,23 @@
+
+
+
+

+
+
+
+
+
+ +
+
+
+
+ +
diff --git a/web/js/CacheUMLExplorer.js b/web/js/CacheUMLExplorer.js index 9ea06a6..0a101ab 100644 --- a/web/js/CacheUMLExplorer.js +++ b/web/js/CacheUMLExplorer.js @@ -18,7 +18,15 @@ var CacheUMLExplorer = function (treeViewContainer, classViewContainer) { zoomInButton: id("button.zoomIn"), zoomOutButton: id("button.zoomOut"), zoomNormalButton: id("button.zoomNormal"), - infoButton: id("button.showInfo") + infoButton: id("button.showInfo"), + methodCodeView: id("methodCodeView"), + closeMethodCodeView: id("closeMethodCodeView"), + methodLabel: id("methodLabel"), + methodCode: id("methodCode"), + classView: id("classView"), + svgContainer: id("svgContainer"), + methodDescription: id("methodDescription"), + methodViewBounds: id("methodViewBounds") }; this.UI = new UI(this); diff --git a/web/js/ClassView.js b/web/js/ClassView.js index bd6309a..8227fb5 100644 --- a/web/js/ClassView.js +++ b/web/js/ClassView.js @@ -128,7 +128,7 @@ ClassView.prototype.createClassInstance = function (name, classMetaData) { self = this; var insertString = function (array, string, extraString) { - array.push(string + (extraString ? extraString : "")); + array.push({ text: string + (extraString ? extraString : "")}); }; var classInstance = new joint.shapes.uml.Class({ @@ -172,7 +172,20 @@ ClassView.prototype.createClassInstance = function (name, classMetaData) { } }, classSigns: this.getClassSigns(classMetaData), - SYMBOL_12_WIDTH: self.SYMBOL_12_WIDTH + SYMBOL_12_WIDTH: self.SYMBOL_12_WIDTH, + attrs: { + ".uml-class-methods-text": { + lineClickHandlers: (function (ps) { + var arr = [], p; + for (p in ps) { + arr.push((function (p) { return function () { + self.showMethodCode(name, p) + }})(p)); + } + return arr; + })(classMethods) + } + } }); this.objects.push(classInstance); @@ -182,6 +195,34 @@ ClassView.prototype.createClassInstance = function (name, classMetaData) { }; +ClassView.prototype.showMethodCode = function (className, methodName) { + + var self = this, + els = this.cacheUMLExplorer.elements; + + this.cacheUMLExplorer.source.getMethod(className, methodName, function (err, data) { + if (err || data.error) { + self.cacheUMLExplorer.UI.displayMessage("Unable to get method \"" + methodName + "\"!"); + return; + } + els.methodLabel.textContent = className + ": " + methodName + "(" + + (data["arguments"] || "").replace(/,/g, ", ").replace(/:/g, ": ") + ")" + + (data["returns"] ? ": " + data["returns"] : ""); + els.methodDescription.innerHTML = data["description"] || ""; + els.methodCode.textContent = data["code"] || ""; + els.methodViewBounds.style.height = + els.classView.offsetHeight - els.methodViewBounds.offsetTop + "px"; + els.methodCodeView.classList.add("active"); + }); + +}; + +ClassView.prototype.hideMethodCode = function () { + + this.cacheUMLExplorer.elements.methodCodeView.classList.remove("active"); + +}; + ClassView.prototype.render = function (data) { var self = this, @@ -448,6 +489,9 @@ ClassView.prototype.init = function () { this.cacheUMLExplorer.elements.zoomNormalButton.addEventListener("click", function () { self.zoom(null); }); + this.cacheUMLExplorer.elements.closeMethodCodeView.addEventListener("click", function () { + self.hideMethodCode(); + }); this.SYMBOL_12_WIDTH = (function () { var e = document.createElementNS("http://www.w3.org/2000/svg", "text"), @@ -460,7 +504,7 @@ ClassView.prototype.init = function () { e.setAttribute("font-size", "12"); e.textContent = "aBcDeFgGhH"; document.body.appendChild(s); - w = e.getBBox().width/10; + w = e.getBBox().width / 10; s.parentNode.removeChild(s); return w; })(); diff --git a/web/js/Source.js b/web/js/Source.js index f66b3d3..ffc84eb 100644 --- a/web/js/Source.js +++ b/web/js/Source.js @@ -15,6 +15,22 @@ Source.prototype.getClassTree = function (callback) { }; +/** + * Return method data. + * @param {string} className + * @param {string} methodName + * @param {Source~dataCallback} callback + */ +Source.prototype.getMethod = function (className, methodName, callback) { + + lib.load( + this.URL + "/GetMethod/" + encodeURIComponent(className) + "/" + + encodeURIComponent(methodName), + null, + callback); + +}; + /** * Return class view. * @param {string} className diff --git a/web/jsLib/joint.js b/web/jsLib/joint.js index 46584ee..41a8aad 100644 --- a/web/jsLib/joint.js +++ b/web/jsLib/joint.js @@ -17229,6 +17229,11 @@ if ( typeof window === "object" && typeof window.document === "object" ) { if (opt.clickHandler) { tspan.node.onclick = opt.clickHandler; } + + if (opt.lineClickHandlers && opt.lineClickHandlers[i]) { + tspan.node.addEventListener("click", opt.lineClickHandlers[i]); + tspan.node.setAttribute("class", tspan.node.getAttribute("class") + " line-clickable"); + } // Make sure the textContent is never empty. If it is, add an additional // space (an invisible character) so that following lines are correctly // relatively positioned. `dy=1em` won't work with empty lines otherwise. diff --git a/web/jsLib/joint.shapes.uml.js b/web/jsLib/joint.shapes.uml.js index 6763788..51d3b6f 100644 --- a/web/jsLib/joint.shapes.uml.js +++ b/web/jsLib/joint.shapes.uml.js @@ -85,20 +85,25 @@ joint.shapes.uml.Class = joint.shapes.basic.Generic.extend({ initialize: function () { - var rects = [ + var o, + rects = [ { type: 'name', text: this.getClassName() }, - { type: 'params', text: this.get('params') }, - { type: 'attrs', text: this.get('attributes') }, - { type: 'methods', text: this.get('methods') } + { type: 'params', text: (o = this.get('params')) .map(function (e) { return e.text; }), o: o }, + { type: 'attrs', text: (o = this.get('attributes')).map(function (e) { return e.text; }), o: o }, + { type: 'methods', text: (o = this.get('methods')) .map(function (e) { return e.text; }), o: o } ], self = this, classSigns = this.get('classSigns'), SYMBOL_12_WIDTH = this.get('SYMBOL_12_WIDTH') || 6.6, - i, blockWidth, left = 3, top = 3, w; + i, j, blockWidth, left = 3, top = 3, w, positions = [], sign; + + var subLabelWidth = function (sign) { // object + return sign.text.length * SYMBOL_12_WIDTH + (sign.icon ? 13 : 0) + }; // preserve space for sub-labels w = 0; for (i in classSigns) { - w += classSigns[i].text.length * SYMBOL_12_WIDTH + (classSigns[i].icon ? 13 : 0) + (i ? 3 : 0); + w += subLabelWidth(classSigns[i]); i = 1; } @@ -116,18 +121,30 @@ joint.shapes.uml.Class = joint.shapes.basic.Generic.extend({ if (classSigns.length) this.HEAD_EMPTY_LINES = 1; + // centering algorithm - first, remember position without centering + j = 0; for (i in classSigns) { w = classSigns[i].text.length*SYMBOL_12_WIDTH + (classSigns[i].icon ? 13 : 0); - if (left + w - 3 > blockWidth) { top += 12; left = 3; this.HEAD_EMPTY_LINES++; } - this.markup += '' + - (classSigns[i].icon ? '' : '') + '' + classSigns[i].text + - ''; + if (left + w - 3 > blockWidth) { top += 12; left = 3; this.HEAD_EMPTY_LINES++; j++; } + if (!positions[j]) positions[j] = []; + positions[j].push({ top: top, left: left, o: classSigns[i] }); left += w + 3; } + // then draw on position with computed seek by X to center content + for (i = 0; i < positions.length; i++) { // repeat positions and draw signs + w = (blockWidth - (sign = positions[i][positions[i].length - 1]).left - subLabelWidth(sign.o)) / 2; + for (j = 0; j < positions[i].length; j++) { + sign = positions[i][j]; + this.markup += '' + + (sign.o.icon ? '' : '') + '' + sign.o.text + + ''; + } + } + this.on('change:name change:attributes change:methods', function () { this.updateRectangles(); this.trigger('uml-update'); @@ -151,9 +168,9 @@ joint.shapes.uml.Class = joint.shapes.basic.Generic.extend({ var rects = [ { type: 'name', text: this.getClassName() }, - { type: 'params', text: this.get('params') }, - { type: 'attrs', text: this.get('attributes') }, - { type: 'methods', text: this.get('methods') } + { type: 'params', text: this.get('params').map(function (e) { return e.text; }) }, + { type: 'attrs', text: this.get('attributes').map(function (e) { return e.text; }) }, + { type: 'methods', text: this.get('methods').map(function (e) { return e.text; }) } ]; var offsetY = 0;