diff --git a/README.md b/README.md index 45090ee..0967e6e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # CacheUMLExplorer -An UML Class explorer for InterSystems Caché. It can build UML class diagram for any class in Caché. +An UML Class explorer for InterSystems Caché. It can build UML class diagram for any class or even for whole package in Caché. ## Screenshots diff --git a/cache/projectTemplate.xml b/cache/projectTemplate.xml index 2906f58..632c8fa 100644 --- a/cache/projectTemplate.xml +++ b/cache/projectTemplate.xml @@ -4,15 +4,55 @@ Class contains methods that return structured class data. -63663,69939 +63668,773.59952 63653,67019.989197 + +1 +%ZEN.proxyObject + level) { + set level = level + 1 + set resp = ##class(%ZEN.proxyObject).%New() + do objects.GetAt(level - 1).%DispatchSetProperty($LISTGET(parts, level - 1), resp) + do objects.SetAt(resp, level) + } + if ($LISTLENGTH(parts) = level) { + do resp.%DispatchSetProperty($LISTGET(parts, level), classes.Data("Hidden")) + } + set lastParts = parts + } + + quit objects.GetAt(1) +]]> + + 1 classDefinition:%Dictionary.ClassDefinition %ZEN.proxyObject do oMeth.%DispatchSetProperty("returns", classDefinition.Methods.GetAt(i).ReturnType) } - return oClass + set oParameters = ##class(%ZEN.proxyObject).%New() + set oClass.parameters = oParameters + set count = classDefinition.Parameters.Count() + for i = 1:1:count { + set oPar = ##class(%ZEN.proxyObject).%New() + do oParameters.%DispatchSetProperty(classDefinition.Parameters.GetAt(i).Name, oPar) + do oPar.%DispatchSetProperty("type", classDefinition.Parameters.GetAt(i).Type) + } + + quit oClass +]]> + + + +1 +typeName:%String +%String + @@ -50,7 +114,7 @@ Class contains methods that return structured class data. } set oInherit = oData.inheritance.%DispatchGetProperty(baseClassDefinition.Name) for i=1:1:$LISTLENGTH(superParts) { - set className = $LISTGET(superParts, i) + set className = ..extendClassFromType($LISTGET(superParts, i)) do oInherit.%DispatchSetProperty(className, 1) if (oData.classes.%DispatchGetProperty(className) = "") { set cdef = ##class(%Dictionary.ClassDefinition).%OpenId(className) @@ -60,13 +124,14 @@ Class contains methods that return structured class data. } } } - return $$$OK + quit $$$OK ]]> 1 baseClassDefinition:%Dictionary.ClassDefinition +%ZEN.proxyObject do ..collectInheritance(oData, baseClassDefinition) - return oData + quit oData +]]> + + + +1 +rootPackageName:%String +%ZEN.proxyObject + - + - - - + + + @@ -94,7 +185,7 @@ Class contains methods that return structured class data. REST interface for UMLExplorer %CSP.REST -63663,76166.562046 +63667,85509.960346 63648,30450.187229 @@ -107,6 +198,7 @@ REST interface for UMLExplorer + ]]> @@ -117,37 +209,7 @@ Method returns whole class tree visible in the current namespace. 1 %Status level) { - set level = level + 1 - set resp = ##class(%ZEN.proxyObject).%New() - do objects.GetAt(level - 1).%DispatchSetProperty($LISTGET(parts, level - 1), resp) - do objects.SetAt(resp, level) - } - if ($LISTLENGTH(parts) = level) { - do resp.%DispatchSetProperty($LISTGET(parts, level), classes.Data("Hidden")) - } - set lastParts = parts - } - - do objects.GetAt(1).%ToJSON(, "ou") + do ##class(UMLExplorer.ClassView).getClassTree().%ToJSON(, "ou") return $$$OK ]]> @@ -167,6 +229,19 @@ Returns classTree by given class name ]]> + + +Returns all package class trees by given package name +1 +packageName:%String +%Status + + + Method to test accessibility of REST interface. diff --git a/gulpfile.js b/gulpfile.js index 6ae71fa..8117730 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -4,6 +4,7 @@ var gulp = require("gulp"), concat = require("gulp-concat"), uglify = require("gulp-uglify"), wrap = require("gulp-wrap"), + stripComments = require("gulp-strip-comments"), addsrc = require('gulp-add-src'), minifyCSS = require("gulp-minify-css"), htmlReplace = require("gulp-html-replace"), @@ -14,6 +15,7 @@ var gulp = require("gulp"), rename = require("gulp-rename"); var banner = [ + "", "/** <%= pkg.name %>", " ** <%= pkg.description %>", " ** @author <%= pkg.author %>", @@ -29,11 +31,10 @@ gulp.task("clean", function () { .pipe(clean()); }); -gulp.task("gatherScripts", ["clean"], function () { - return gulp.src("web/js/*.js") - .pipe(concat("CacheUMLExplorer.js")) - .pipe(replace(/\/\*\{\{replace:version}}\*\//, "\"" + pkg["version"] + "\"")) - .pipe(wrap("CacheUMLExplorer = (function(){<%= contents %> return CacheUMLExplorer;}());")) +gulp.task("gatherLibs", ["clean"], function () { + return gulp.src([ + "web/jsLib/joint.shapes.uml.js" + ]) .pipe(uglify({ output: { ascii_only: true, @@ -41,12 +42,29 @@ gulp.task("gatherScripts", ["clean"], function () { max_line_len: 30000 } })) - .pipe(header(banner, { pkg: pkg })) .pipe(addsrc.prepend([ "web/jsLib/joint.min.js", - "web/jsLib/joint.shapes.uml.js", "web/jsLib/joint.layout.DirectedGraph.min.js" ])) + .pipe(stripComments({ safe: true })) + .pipe(concat("CacheUMLExplorer.js")) + .pipe(gulp.dest("build/web/js/")); +}); + +gulp.task("gatherScripts", ["clean", "gatherLibs"], function () { + return gulp.src("web/js/*.js") + .pipe(concat("CacheUMLExplorer.js")) + .pipe(replace(/[^\s]+\/\*build.replace:(.*)\*\//g, "$1")) + .pipe(wrap("CacheUMLExplorer = (function(){<%= contents %> return CacheUMLExplorer;}());")) + .pipe(uglify({ + output: { + ascii_only: true, + width: 30000, + max_line_len: 30000 + } + })) + .pipe(header(banner, { pkg: pkg })) + .pipe(addsrc.prepend("build/web/js/CacheUMLExplorer.js")) .pipe(concat("CacheUMLExplorer.js")) .pipe(gulp.dest("build/web/js/")); }); @@ -90,7 +108,7 @@ gulp.task("exportCacheXML", [ /\{\{replace:js}}/, function () { return fs.readFileSync("build/web/js/CacheUMLExplorer.js", "utf-8"); } )) - .pipe(rename(function (path) { path.basename += "-v" + pkg["version"]; })) + .pipe(rename(function (path) { path.basename = "CacheUMLExplorer-v" + pkg["version"]; })) .pipe(gulp.dest("build/Cache")); }); diff --git a/package.json b/package.json index 213f0b0..e33b3eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CacheUMLExplorer", - "version": "0.2", + "version": "0.3.0", "description": "An UML Class explorer for InterSystems Caché", "directories": { "test": "test" @@ -17,6 +17,7 @@ "gulp-minify-css": "^0.3.11", "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.0", + "gulp-strip-comments": "^1.0.1", "gulp-uglify": "^1.0.1", "gulp-wrap": "^0.5.0", "gulp-zip": "^2.0.2" diff --git a/web/css/extras.css b/web/css/extras.css index ca75777..13c99ae 100644 --- a/web/css/extras.css +++ b/web/css/extras.css @@ -33,8 +33,8 @@ width: 30px; height: 30px; border-radius: 15px; - background: white; - box-shadow: 0 0 5px white; + background: whitesmoke; + box-shadow: 0 0 5px whitesmoke; z-index: 1; } @@ -60,4 +60,112 @@ 100% { transform: rotate(360deg); } +} + +.icon { + display: inline-block; + background-color: #333; + border-radius: 12px; + width: 24px; + height: 24px; + position: relative; + cursor: pointer; + -webkit-transition: all .2s ease; + -moz-transition: all .2s ease; + -o-transition: all .2s ease; + transition: all .2s ease; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.icon:hover { + box-shadow: 0 0 5px 2px #ffcc1b; +} + +.icon.plus:before { + content: ""; + background-color: #fff; + width: 4px; + height: 14px; + border-radius: 1px; + position: absolute; + top: 5px; + left: 10px; +} + +.icon.plus:after { + content: ""; + background-color: #fff; + width: 14px; + height: 4px; + border-radius: 1px; + position: absolute; + top: 10px; + left: 5px; +} + +.icon.minus:after { + content: ""; + background-color: #fff; + width: 14px; + height: 4px; + border-radius: 1px; + position: absolute; + top: 10px; + left: 5px; +} + +.icon.scaleNormal:after { + content: "1:1"; + position: absolute; + color: #fff; + text-align: center; + font-weight: 900; + font-family: monospace; + font-size: 10px; + width: 100%; + top: 6px; + left: 0; +} + +.icon.list { + position: relative; + width: 30px; + height: 20px; + background-color: transparent; +} + +.icon.list:before { + content: ""; + top: 8px; + width: 18px; + height: 4px; + left: 8px; + box-shadow: inset 0 0 0 32px black, 0 -7px 0 0 black, 0 7px 0 0 black; + position: absolute; +} + +.icon.list:after { + content: ""; + left: 0; + top: 8px; + width: 4px; + height: 4px; + box-shadow: inset 0 0 0 32px black, 0 -7px 0 0 black, 0 7px 0 0 black; + position: absolute; + -webkit-transition: all .2s ease; + -moz-transition: all .2s ease; + -o-transition: all .2s ease; + transition: all .2s ease; +} + +.icon.list:hover { + box-shadow: none; +} + +.icon.list:hover:after { + left: 4px; + box-shadow: inset 0 0 0 32px #ffcc1b, 0 -7px 0 0 #ffcc1b, 0 7px 0 0 #ffcc1b; } \ No newline at end of file diff --git a/web/css/interface.css b/web/css/interface.css index 7228824..5f4bc80 100644 --- a/web/css/interface.css +++ b/web/css/interface.css @@ -29,12 +29,18 @@ html, body { position: absolute; top: 0; left: 0; - width: 100%; padding: .5em; font-weight: 600; font-size: 18pt; } +.ui-toolBar { + position: absolute; + bottom: 0; + right: 0; + padding: .5em; +} + #className { text-shadow: 1px 1px 0 white, -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white; } \ No newline at end of file diff --git a/web/css/treeView.css b/web/css/treeView.css index 91afac9..eb231b2 100644 --- a/web/css/treeView.css +++ b/web/css/treeView.css @@ -12,10 +12,11 @@ } .tv-class-name.selected { - box-shadow: inset 0 0 2px 2px deepskyblue; + box-shadow: inset 0 0 2px 2px #ffcc1b; } .tv-class-name, .tv-package-name { + position: relative; padding: 3px; cursor: pointer; border-radius: 5px; @@ -26,11 +27,15 @@ } .tv-class-name:hover, .tv-package-name:hover { - background: #b9ffff; + background: #ffcc1b; font-weight: 900; padding-left: 30px; } +.tv-package-name:hover { + background: #7dcdeb; +} + .tv-package-name { position: relative; padding-left: 20px; @@ -96,4 +101,15 @@ .tv-package .tv-package-content { padding-left: 20px; +} + +.tv-rightListIcon { + position: absolute !important; + top: 3px; + opacity: 0; + right: 0; +} + +.tv-package-name:hover .tv-rightListIcon { + opacity: 1; } \ No newline at end of file diff --git a/web/index.html b/web/index.html index 0e2baca..4b1ca2d 100644 --- a/web/index.html +++ b/web/index.html @@ -2,7 +2,7 @@ - Caché UML explorer + Cache UML explorer @@ -32,6 +32,11 @@
+
+
+
+
+
diff --git a/web/js/CacheUMLExplorer.js b/web/js/CacheUMLExplorer.js index c39648b..c84cf91 100644 --- a/web/js/CacheUMLExplorer.js +++ b/web/js/CacheUMLExplorer.js @@ -11,7 +11,10 @@ var CacheUMLExplorer = function (treeViewContainer, classViewContainer) { this.elements = { className: document.getElementById("className"), treeViewContainer: treeViewContainer, - classViewContainer: classViewContainer + classViewContainer: classViewContainer, + zoomInButton: document.getElementById("button.zoomIn"), + zoomOutButton: document.getElementById("button.zoomOut"), + zoomNormalButton: document.getElementById("button.zoomNormal") }; this.source = new Source(); diff --git a/web/js/ClassTree.js b/web/js/ClassTree.js index 658a25c..32df7e9 100644 --- a/web/js/ClassTree.js +++ b/web/js/ClassTree.js @@ -34,8 +34,6 @@ ClassTree.prototype.removeLoader = function () { ClassTree.prototype.classSelected = function (element, className) { - var self = this; - this.SELECTED_CLASS_NAME = className; if (element !== this.SELECTED_ELEMENT) { @@ -52,6 +50,22 @@ ClassTree.prototype.classSelected = function (element, className) { }; +ClassTree.prototype.packageSelected = function (element, packageName) { + + if (element !== this.SELECTED_ELEMENT) { + if (this.SELECTED_ELEMENT) this.SELECTED_ELEMENT.classList.remove("selected"); + this.SELECTED_ELEMENT = element; + } + + if (!element.classList.contains("selected")) { + element.classList.add("selected"); + this.cacheUMLExplorer.classView.loadPackage(packageName); + } + + this.cacheUMLExplorer.elements.className.textContent = packageName; + +}; + ClassTree.prototype.updateTree = function (treeObject) { var self = this, @@ -80,7 +94,7 @@ ClassTree.prototype.updateTree = function (treeObject) { var append = function (rootElement, elementName, isPackage, path) { var el1 = div(), - el2, el3; + el2, el3, el4; if (isPackage) { el1.className = "tv-package"; @@ -88,6 +102,11 @@ ClassTree.prototype.updateTree = function (treeObject) { (el3 = div()).className = "tv-package-content"; el1.appendChild(el2); el1.appendChild(el3); el2.addEventListener("click", packageClick); + el2.appendChild(el4 = div()); + el4.className = "tv-rightListIcon icon list"; + el4.addEventListener("click", function () { + self.packageSelected(el1, (path ? path + "." : path) + elementName); + }); } else { el1.className = "tv-class-name"; el1.textContent = elementName; diff --git a/web/js/ClassView.js b/web/js/ClassView.js index 9206663..ee2e270 100644 --- a/web/js/ClassView.js +++ b/web/js/ClassView.js @@ -14,6 +14,10 @@ var ClassView = function (parent, container) { this.links = []; this.objects = []; + this.PAPER_SCALE = 1; + this.MIN_PAPER_SCALE = 0.2; + this.MAX_PAPER_SCALE = 5; + this.init(); }; @@ -56,10 +60,59 @@ ClassView.prototype.resetView = function () { }; +/** + * @param {string} name + * @param classMetaData + * @returns {joint.shapes.uml.Class} + */ +ClassView.prototype.createClassInstance = function (name, classMetaData) { + + var attrArr, methArr, + classParams = classMetaData["parameters"], + classProps = classMetaData["properties"], + classMethods = classMetaData["methods"]; + + var insertString = function (array, string) { + string.match(/.{1,44}/g).forEach(function (p) { + array.push(p); + }); + }; + + return new joint.shapes.uml.Class({ + name: name, + attributes: attrArr = (function (params, ps) { + var arr = [], n; + for (n in params) { + insertString(arr, n + (params[n]["type"] ? ": " + params[n]["type"] : "")); + } + for (n in ps) { + insertString( + arr, + (ps[n]["private"] ? "- " : "+ ") + n + + (ps[n]["type"] ? ": " + ps[n]["type"] : "") + ); + } + return arr; + })(classParams, classProps), + methods: methArr = (function (ps) { + var arr = [], n; + for (n in ps) { + insertString(arr, "+ " + n + (ps[n]["returns"] ? ": " + ps[n]["returns"] : "")); + } + return arr; + })(classMethods), + size: { + width: 300, + height: Math.max(attrArr.length*12.1, 15) + Math.max(methArr.length*12.1, 15) + 40 + } + }); + +}; + ClassView.prototype.render = function (data) { - var p, pp, className, classProps, classMethods, classInstance, - uml = joint.shapes.uml, attrArr, methArr, relFrom, relTo, + var p, pp, className, classInstance, + uml = joint.shapes.uml, relFrom, relTo, classes = {}, connector; if (!data["classes"]) { @@ -68,36 +121,7 @@ ClassView.prototype.render = function (data) { } for (className in data["classes"]) { - classProps = data["classes"][className]["properties"]; - classMethods = data["classes"][className]["methods"]; - - classInstance = new uml.Class({ - name: className, - attributes: attrArr = (function (ps) { - var arr = [], n, s; - for (n in ps) { - s = (ps[n]["private"] ? "- " : "+ ") + n + ": " + ps[n]["type"]; - s.match(/.{1,44}/g).forEach(function (p) { - arr.push(p); - }); - } - return arr; - })(classProps), - methods: methArr = (function (ps) { - var arr = [], n, s; - for (n in ps) { - s = "+ " + n + (ps[n]["returns"] ? ": " + ps[n]["returns"] : ""); - s.match(/.{1,44}/g).forEach(function (p) { - arr.push(p); - }); - } - return arr; - })(classMethods), - size: { - width: 300, - height: Math.max(attrArr.length*12.1, 15) + Math.max(methArr.length*12.1, 15) + 40 - } - }); + classInstance = this.createClassInstance(className, data["classes"][className]); this.objects.push(classInstance); classes[className] = { instance: classInstance @@ -108,7 +132,7 @@ ClassView.prototype.render = function (data) { } for (p in data["inheritance"]) { - relFrom = classes[p].instance; + relFrom = (classes[p] || {}).instance; for (pp in data["inheritance"][p]) { relTo = (classes[pp] || {}).instance; if (relFrom && relTo) { @@ -149,7 +173,25 @@ ClassView.prototype.loadClass = function (className) { self.removeLoader(); if (err) { self.showLoader("Unable to get " + self.cacheUMLExplorer.classTree.SELECTED_CLASS_NAME); - console.error(err); + console.error.call(console, err); + } else { + self.cacheUMLExplorer.classView.render(data); + } + }); + +}; + +ClassView.prototype.loadPackage = function (packageName) { + + var self = this; + + this.showLoader(); + this.cacheUMLExplorer.source.getPackageView(packageName, function (err, data) { + //console.log(data); + self.removeLoader(); + if (err) { + self.showLoader("Unable to get package " + packageName); + console.error.call(console, err); } else { self.cacheUMLExplorer.classView.render(data); } @@ -161,6 +203,33 @@ ClassView.prototype.updateSizes = function () { this.paper.setDimensions(this.container.offsetWidth, this.container.offsetHeight); }; +/** + * Scale view according to delta. + * + * @param {number|string} delta + */ +ClassView.prototype.zoom = function (delta) { + + var scaleOld = this.PAPER_SCALE, scaleDelta; + if (typeof delta === "number") { + this.PAPER_SCALE += delta *Math.min( + 0.5, + Math.abs(this.PAPER_SCALE - (delta < 0 ? this.MIN_PAPER_SCALE : this.MAX_PAPER_SCALE))/2 + ); + } else { + this.PAPER_SCALE = 1; + } + this.paper.scale(this.PAPER_SCALE, this.PAPER_SCALE); + scaleDelta = this.PAPER_SCALE - scaleOld; + this.paper.setOrigin( + this.paper.options.origin.x + - scaleDelta*this.cacheUMLExplorer.elements.classViewContainer.offsetWidth/2, + this.paper.options.origin.y + - scaleDelta*this.cacheUMLExplorer.elements.classViewContainer.offsetHeight/2 + ); + +}; + ClassView.prototype.init = function () { var p, self = this, @@ -216,6 +285,18 @@ ClassView.prototype.init = function () { this.cacheUMLExplorer.elements.classViewContainer.addEventListener("mousemove", moveHandler); this.cacheUMLExplorer.elements.classViewContainer.addEventListener("touchmove", moveHandler); + this.cacheUMLExplorer.elements.classViewContainer.addEventListener("mousewheel", function (e) { + self.zoom(Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)))); + }); + this.cacheUMLExplorer.elements.zoomInButton.addEventListener("click", function () { + self.zoom(1); + }); + this.cacheUMLExplorer.elements.zoomOutButton.addEventListener("click", function () { + self.zoom(-1); + }); + this.cacheUMLExplorer.elements.zoomNormalButton.addEventListener("click", function () { + self.zoom(null); + }); //var classes = { // diff --git a/web/js/Source.js b/web/js/Source.js index 72fdfbb..f66b3d3 100644 --- a/web/js/Source.js +++ b/web/js/Source.js @@ -1,6 +1,7 @@ var Source = function () { - this.URL = "http://localhost:57773/UMLExplorer"; + this.URL = window.location.protocol + "//" + window.location.hostname + ":" + + 57773/*build.replace:window.location.port*/ + "/UMLExplorer"; }; @@ -25,6 +26,17 @@ Source.prototype.getClassView = function (className, callback) { }; +/** + * Return class view. + * @param {string} packageName + * @param {Source~dataCallback} callback + */ +Source.prototype.getPackageView = function (packageName, callback) { + + lib.load(this.URL + "/GetPackageView/" + encodeURIComponent(packageName), null, callback); + +}; + /** * This callback handles data received directly from server. * @callback Source~dataCallback diff --git a/web/jsLib/joint.layout.DirectedGraph.min.js b/web/jsLib/joint.layout.DirectedGraph.min.js index e3ee1bf..fe2b704 100644 --- a/web/jsLib/joint.layout.DirectedGraph.min.js +++ b/web/jsLib/joint.layout.DirectedGraph.min.js @@ -1,4 +1,4 @@ -/*! JointJS v0.9.3 - JavaScript diagramming library 2015-02-03 +/* JointJS v0.9.3 - JavaScript diagramming library 2015-02-03 This Source Code Form is subject to the terms of the Mozilla Public diff --git a/web/jsLib/joint.shapes.uml.js b/web/jsLib/joint.shapes.uml.js index b9adfc3..3f90237 100644 --- a/web/jsLib/joint.shapes.uml.js +++ b/web/jsLib/joint.shapes.uml.js @@ -1,4 +1,4 @@ -/*! JointJS v0.9.3 - JavaScript diagramming library 2015-02-03 +/* JointJS v0.9.3 - JavaScript diagramming library 2015-02-03 This Source Code Form is subject to the terms of the Mozilla Public @@ -40,9 +40,9 @@ joint.shapes.uml.Class = joint.shapes.basic.Generic.extend({ attrs: { rect: { 'width': 200 }, - '.uml-class-name-rect': { 'stroke': 'black', 'stroke-width': 2, 'fill': '#3498db' }, - '.uml-class-attrs-rect': { 'stroke': 'black', 'stroke-width': 2, 'fill': '#2980b9' }, - '.uml-class-methods-rect': { 'stroke': 'black', 'stroke-width': 2, 'fill': '#2980b9' }, + '.uml-class-name-rect': { 'stroke': 'black', 'stroke-width': 1, 'fill': '#3498db' }, + '.uml-class-attrs-rect': { 'stroke': 'black', 'stroke-width': 1, 'fill': '#2980b9' }, + '.uml-class-methods-rect': { 'stroke': 'black', 'stroke-width': 1, 'fill': '#2980b9' }, '.uml-class-name-text': { 'ref': '.uml-class-name-rect', 'ref-y': .5, 'ref-x': .5, 'text-anchor': 'middle', 'y-alignment': 'middle', 'font-weight': 'bold',