diff --git a/README b/README new file mode 100644 index 0000000..0f82e35 --- /dev/null +++ b/README @@ -0,0 +1,37 @@ +GUI-DAT +======= + +Usage: +------ + + var controllableObject = + { + numberProperty: 20, + anotherNumberProperty: 0, + textProperty: "a string", + booleanProperty: false, + functionProperty: function() { + alert("I am a function!"); + } + }; + + window.onload = function() { + + GUI.start(); + + // Creates a number box + GUI.add(controllableObject, "numberProperty"); + + // Creates a slider (min, max) + GUI.add(controllableObject, "anotherNumberProperty", -100, 100); + + // Creates a text field + GUI.add(controllableObject, "textProperty"); + + // Creates a checkbox + GUI.add(controllableObject, "booleanProperty"); + + // Creates a button + GUI.add(controllableObject, "functionProperty"); + + } \ No newline at end of file diff --git a/controller.boolean.js b/controller.boolean.js new file mode 100644 index 0000000..3acc78f --- /dev/null +++ b/controller.boolean.js @@ -0,0 +1,25 @@ +var BooleanController = function() { + this.type = "boolean"; + Controller.apply(this, arguments); + + var _this = this; + var input = document.createElement('input'); + input.setAttribute('type', 'checkbox'); + + this.domElement.addEventListener('click', function(e) { + input.checked = !input.checked; + e.preventDefault(); + _this.setValue(input.checked); + }, false); + + input.addEventListener('mouseup', function(e) { + input.checked = !input.checked; // counteracts default. + }, false); + + this.domElement.style.cursor = "pointer"; + this.propertyNameElement.style.cursor = "pointer"; + this.domElement.appendChild(input); + +}; +BooleanController.prototype = new Controller(); +BooleanController.prototype.constructor = BooleanController; \ No newline at end of file diff --git a/controller.function.js b/controller.function.js new file mode 100644 index 0000000..d523562 --- /dev/null +++ b/controller.function.js @@ -0,0 +1,12 @@ +var FunctionController = function() { + this.type = "function"; + var _this = this; + Controller.apply(this, arguments); + this.domElement.addEventListener('click', function() { + _this.object[_this.propertyName].call(_this.object); + }, false); + this.domElement.style.cursor = "pointer"; + this.propertyNameElement.style.cursor = "pointer"; +}; +FunctionController.prototype = new Controller(); +FunctionController.prototype.constructor = FunctionController; \ No newline at end of file diff --git a/controller.js b/controller.js new file mode 100644 index 0000000..719b420 --- /dev/null +++ b/controller.js @@ -0,0 +1,42 @@ +var Controller = function() { + + this.setName = function(n) { + this.propertyNameElement.innerHTML = n; + } + + this.setValue = function(n) { + this.object[this.propertyName] = n; + } + + this.getValue = function() { + return this.object[this.propertyName]; + } + + this.makeUnselectable = function(elem) { + elem.onselectstart = function() { return false; }; + elem.style.MozUserSelect = "none"; + elem.style.KhtmlUserSelect = "none"; + elem.unselectable = "on"; + } + + this.makeSelectable = function(elem) { + elem.onselectstart = function() { }; + elem.style.MozUserSelect = "auto"; + elem.style.KhtmlUserSelect = "auto"; + elem.unselectable = "off"; + } + + this.domElement = document.createElement('div'); + this.domElement.setAttribute('class', 'guidat-controller ' + this.type); + + this.object = arguments[0]; + this.propertyName = arguments[1]; + + this.propertyNameElement = document.createElement('span'); + this.propertyNameElement.setAttribute('class', 'guidat-propertyname'); + this.setName(this.propertyName); + this.domElement.appendChild(this.propertyNameElement); + + this.makeUnselectable(this.domElement); + +}; \ No newline at end of file diff --git a/controller.number.js b/controller.number.js new file mode 100644 index 0000000..be428ed --- /dev/null +++ b/controller.number.js @@ -0,0 +1,132 @@ +// TODO: How do we intercept the press up/down event on number fields? +// TODO: Provide alternate controllers for non-html5 browsers? +// TODO: Firefox is retarded? +var NumberController = function() { + + this.type = "number"; + + Controller.apply(this, arguments); + + var _this = this; + + // If we simply click and release a number field, we want to highlight it. + // This variable keeps track of whether or not we've draggedNumberField. + var draggedNumberField = false; + var clickedNumberField = false; + + var y = py = 0; + + var min = arguments[2]; + var max = arguments[3]; + var step = arguments[4]; + + if (!step) { + if (min && max) { + step = (max-min)*0.01; + } else { + step = 1; + } + } + + var numberField = document.createElement('input'); + numberField.setAttribute('id', this.propertyName); + + // Little up and down arrows are pissing me off. + numberField.setAttribute('type', 'text'); + numberField.setAttribute('value', this.getValue()); + + if (step) numberField.setAttribute('step', step); + + this.domElement.appendChild(numberField); + + var slider; + + if (min && max && + (navigator.appVersion.indexOf("chrome") != -1 || navigator.appVersion.indexOf("Safari") != -1)) { + + slider = document.createElement('input'); + slider.setAttribute('type', 'range'); + slider.setAttribute('value', this.getValue()); + slider.setAttribute('min', min); + slider.setAttribute('max', max); + + slider.setAttribute('step', step); + slider.addEventListener('change', function(e) { + updateValue(this.value); + }, false); + this.domElement.appendChild(slider); + } + + numberField.addEventListener('blur', function(e) { + var val = parseFloat(this.value); + if (!isNaN(val)) { + updateValue(val); + } + }, false); + + numberField.addEventListener('mousewheel', function(e) { + e.preventDefault(); + updateValue(_this.getValue() + Math.abs(e.wheelDeltaY)/e.wheelDeltaY*step); + return false; + }, false); + + numberField.addEventListener('mousedown', function(e) { + py = y = e.pageY; + clickedNumberField = true; + document.addEventListener('mousemove', dragNumberField, false); + }, false); + + document.addEventListener('mouseup', function(e) { + document.removeEventListener('mousemove', dragNumberField, false); + _this.makeSelectable(GUI.domElement); + _this.makeSelectable(numberField); + if (clickedNumberField && !draggedNumberField) { + numberField.focus(); + numberField.select(); + } + draggedNumberField = false; + clickedNumberField = false; + }, false); + + if(navigator.appVersion.indexOf('chrome') != -1) { + document.addEventListener('mouseout', function(e) { + document.removeEventListener('mousemove', dragNumberField, false); + }, false); + } + + var dragNumberField = function(e) { + draggedNumberField = true; + e.preventDefault(); + + // We don't want to be highlighting this field as we scroll. + // Or any other fields in this gui for that matter ... + // TODO: Make makeUselectable go through each element and child element. + _this.makeUnselectable(GUI.domElement); + _this.makeUnselectable(numberField); + + py = y; + y = e.pageY; + var dy = py - y; + var newVal = _this.getValue() + dy*step; + updateValue(newVal); + return false; + } + + var updateValue = function(val) { + + val = parseFloat(val); + + if (min && val <= min) { + val = min; + } else if (max && val >= max) { + val = max; + } + _this.setValue(val); + numberField.value = _this.getValue(); + if (slider) slider.value = _this.getValue(); + } + +}; + +NumberController.prototype = new Controller(); +NumberController.prototype.constructor = NumberController; \ No newline at end of file diff --git a/controller.string.js b/controller.string.js new file mode 100644 index 0000000..aa955ea --- /dev/null +++ b/controller.string.js @@ -0,0 +1,27 @@ +var StringController = function() { + + this.type = "string"; + + var _this = this; + + Controller.apply(this, arguments); + + var input = document.createElement('input'); + + var initialValue = this.getValue(); + + input.setAttribute('value', initialValue); + input.setAttribute('spellcheck', 'false'); + this.domElement.addEventListener('mouseup', function() { + input.focus(); + input.select(); + }, false); + + input.addEventListener('keyup', function() { + _this.setValue(input.value); + }, false); + + this.domElement.appendChild(input); +}; +StringController.prototype = new Controller(); +StringController.prototype.constructor = StringController; \ No newline at end of file diff --git a/demo.css b/demo.css new file mode 100644 index 0000000..d46643a --- /dev/null +++ b/demo.css @@ -0,0 +1,37 @@ +pre { +padding: 10px; +border: 1px solid #ccc; +max-width: 500px; +} + +/* Pretty printing styles. Used with prettify.js. */ + +/* SPAN elements with the classes below are added by prettyprint. */ +.str { color: #080; } +.kwd { color: #008; } +.com { color: #800; } +.typ { color: #606; } +.lit { color: #066; } +.pun, .opn, .clo { color: #660; } +.pln { color: #000; } +.tag { color: #008; } +.atn { color: #606; } +.atv { color: #080; } +.dec { color: #606; } + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L5, +li.L6, +li.L7, +li.L8 { list-style-type: none } +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { background: #eee } diff --git a/gui-bare.css b/gui-bare.css new file mode 100644 index 0000000..a61a1f2 --- /dev/null +++ b/gui-bare.css @@ -0,0 +1,26 @@ +#guidat { + position: fixed; + width: 250px; + z-index: 200; + top: 0; + left: 100%; + margin-left: -270px; +} + +#guidat-controllers { + height: 300px; + overflow-y: auto; +} + +#guidat-toggle { + cursor: pointer; +} + +.guidat-controller { + clear: both; +} + +.guidat-controller input { + float: right; + clear: both; +} diff --git a/gui.css b/gui.css new file mode 100644 index 0000000..23b95c2 --- /dev/null +++ b/gui.css @@ -0,0 +1,112 @@ +#guidat { + font: 9px Monaco, monospace; + color: #fff; + position: fixed; + width: 320px; + z-index: 200; + opacity: 0.95; + top: 0; + left: 100%; + margin-left: -340px; + background-color: #fff; + -moz-transition: margin-top .2s ease-out; + -webkit-transition: margin-top .2s ease-out; + transition: margin-top .2s ease-out; + -webkit-box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); +} + +#guidat-controllers { + height: 300px; + overflow-y: auto; + overflow-x: hidden; + background-color: #eee; +} + +#guidat-toggle { + text-decoration: none; + cursor: pointer; + color: #fff; + background-color: #222; + text-align: center; + display: block; + padding: 5px; +} + +#guidat-toggle:hover { + background-color: #000; +} + +.guidat-controller { + padding: 5px; + height: 23px; + clear: left; + border-bottom: 1px solid #222; + background-color: #111; +} + +.guidat-controller.boolean:hover, +.guidat-controller.function:hover { + background-color: #000; +} + +.guidat-controller input { + float: right; + outline: none; + border: 0; + padding: 2px; +} + +.guidat-controller.number { + border-left: 5px solid #00aeff ; +} + +.guidat-controller.string { + border-left: 5px solid #1ed36f; +} + +.guidat-controller.string input { + border: 0; + text-align: right; + color: #1ed36f; + + background-color: rgba(0,0,0,0); +} + +.guidat-controller.boolean { + border-left: 5px solid #54396e; +} + +.guidat-controller.function { + border-left: 5px solid #e61d5f; +} + +.guidat-controller.number input[type=text] { + width: 45px; + margin-left: 10px; +} + +.guidat-controller.number input[type=slider] { + width: 50%; + +} + +.guidat-controller.boolean input { +margin-top: 6px; +} + + + +.guidat-controller:last-child { + border-bottom: none; + -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1); +} + +.guidat-propertyname { + padding: 5px; + cursor: default; + display: inline-block; +} \ No newline at end of file diff --git a/gui.js b/gui.js new file mode 100644 index 0000000..bd6f321 --- /dev/null +++ b/gui.js @@ -0,0 +1,163 @@ +var GUI = new function() { + + var _this = this; + + var controllers = []; + + this.add = function() { + + // We need to call GUI.start() before .add() + if (!started) { + error("Make sure to call GUI.start() in the window.onload function"); + return; + } + + var object = arguments[0]; + var propertyName = arguments[1]; + + // Have we already added this? + if (alreadyControlled(object, propertyName)) { + // error("Controller for \"" + propertyName+"\" already added."); + // return; + } + + var value = object[propertyName]; + + // Does this value exist? Is it accessible? + if (value == undefined) { + error(object + " either has no property \""+propertyName+"\", or the property is inaccessible."); + return; + } + + var type = typeof value; + var handler = addHandlers[type]; + + // Do we know how to deal with this data type? + if (handler == undefined) { + error("Cannot create controller for data type \""+type+"\""); + return; + } + + var controllerObject = handler.apply(this, arguments); + + // Were we able to make the controller? + if (!controllerObject) { + error("Error creating controller for \""+propertyName+"\"."); + return; + } + + // Success. + controllerContainer.appendChild(controllerObject.domElement); + controllers.push(controllerObject); + + return controllerObject; + + } + + var addHandlers = { + + "number": function() { + return construct(NumberController, arguments); + }, + + "string": function() { + return construct(StringController, arguments); + }, + + "boolean": function() { + return construct(BooleanController, arguments); + }, + + "function": function() { + return construct(FunctionController, arguments); + }, + + }; + + var alreadyControlled = function(object, propertyName) { + for (var i in controllers) { + if (controllers[i].object == object && + controllers[i].propertyName == propertyName) { + return true; + } + } + return false; + }; + + var error = function(str) { + if (typeof console.log == 'function') { + console.error("[GUI ERROR] " + str); + } + }; + + var construct = function(constructor, args) { + function F() { + return constructor.apply(this, args); + } + F.prototype = constructor.prototype; + return new F(); + }; + + + + // GUI ... GUI + + this.domElement = null; + var controllerContainer; + var started = false; + var open = false; + + // TODO: obtain this dynamically? + var domElementMarginTop = 300; + + this.start = function() { + + this.domElement = document.createElement('div'); + this.domElement.setAttribute('id', 'guidat'); + + controllerContainer = document.createElement('div'); + controllerContainer.setAttribute('id', 'guidat-controllers'); + + toggleButton = document.createElement('a'); + toggleButton.setAttribute('id', 'guidat-toggle'); + toggleButton.setAttribute('href', '#'); + toggleButton.innerHTML = "Show Controls"; + toggleButton.addEventListener('click', function(e) { + _this.toggle(); + e.preventDefault(); + }, false); + + this.domElement.appendChild(controllerContainer); + this.domElement.appendChild(toggleButton); + + this.domElement.style.marginTop = -domElementMarginTop+"px"; + + document.body.appendChild(this.domElement); + + started = true; + + }; + + this.toggle = function() { + + if (open) { + this.hide(); + } else { + this.show(); + } + + }; + + this.show = function() { + this.domElement.style.marginTop = 0+"px"; + toggleButton.innerHTML = "Hide Controls"; + open = true; + } + + this.hide = function() { + this.domElement.style.marginTop = -domElementMarginTop+"px"; + toggleButton.innerHTML = "Show Controls"; + open = false; + } + +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..05fc2e2 --- /dev/null +++ b/index.html @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + +
+var controllableObject = 
+   {   
+      numberProperty: 20,
+      constrainedNum: 0,
+      textProperty: "a string",
+      booleanProperty: false,
+      functionProperty: function() {
+         alert("I am a function!");
+      }
+   };
+
+window.onload = function() {
+
+   GUI.start();
+   
+   // Creates a number box
+   GUI.add(controllableObject, "numberProperty");
+   
+   // Creates a slider (min, max)
+   GUI.add(controllableObject, "constrainedNum", -100, 100, 0);
+   
+   // Creates a text field
+   GUI.add(controllableObject, "textProperty");
+   
+   // Creates a checkbox
+   GUI.add(controllableObject, "booleanProperty");
+   
+   // Creates a button
+   GUI.add(controllableObject, "functionProperty")
+      .setName("Fire a Function");
+
+};
+
+ + \ No newline at end of file diff --git a/slider.js b/slider.js new file mode 100644 index 0000000..2cb5821 --- /dev/null +++ b/slider.js @@ -0,0 +1,5 @@ +var Slider = function() { + + this.domElement = document.createElement('canvas'); + +} \ No newline at end of file