blob: a2e0ab6514c59a6d2ee464af5595484debb91169 [file] [log] [blame]
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.
* See the License for the specific language governing rights and
* limitations under the License.
*
* The Original Code is Bespin.
*
* The Initial Developer of the Original Code is Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Bespin Team (bespin@mozilla.com)
*
* ***** END LICENSE BLOCK ***** */
dojo.provide("th.th");
/*
Constants
*/
dojo.mixin(th, {
VERTICAL: "v",
HORIZONTAL: "h"
});
/*
Event bus; all listeners and events pass through a single global instance of this class.
*/
dojo.declare("th.Bus", null, {
constructor: function() {
// map of event name to listener; listener contains a selector, function, and optional context object
this.events = {};
},
// register a listener with an event
bind: function(event, selector, listenerFn, listenerContext) {
var listeners = this.events[event];
if (!listeners) {
listeners = [];
this.events[event] = listeners;
}
selector = dojo.isArray(selector) ? selector : [ selector ];
for (var z = 0; z < selector.length; z++) {
for (var i = 0; i < listeners.length; i++) {
if (listeners[i].selector == selector[z] && listeners[i].listenerFn == listenerFn) return;
}
listeners.push({ selector: selector[z], listenerFn: listenerFn, context: listenerContext });
}
},
// removes any listeners whose selectors have the *same identity* as the passed selector
unbind: function(selector) {
for (var event in this.events) {
var listeners = this.events[event];
for (var i = 0; i < listeners.length; i++) {
if (listeners[i].selector === selector) {
this.events[event] = dojo.filter(listeners, function(item){ return item != listeners[i]; });
listeners = this.events[event];
i--;
}
}
}
},
// notify all listeners of an event
fire: function(eventName, eventDetails, component) {
var listeners = this.events[eventName];
if (!listeners || listeners.length == 0) return;
// go through each listener registered for the fired event and check if the selector matches the component for whom
// the event was fired; if there is a match, dispatch the event
for (var i = 0; i < listeners.length; i++) {
// if the listener selector is a string...
if (listeners[i].selector.constructor == String) {
// check if the string starts with a hash, indicating that it should match by id
if (listeners[i].selector.charAt(0) == "#") {
if (component.id == listeners[i].selector.substring(1)) {
this.dispatchEvent(eventName, eventDetails, component, listeners[i]);
}
// otherwise check if it's the name of the component class
} else if (listeners[i].selector == component.declaredClass) {
this.dispatchEvent(eventName, eventDetails, component, listeners[i]);
}
// otherwise check if the selector is the current component
} else if (listeners[i].selector == component) {
this.dispatchEvent(eventName, eventDetails, component, listeners[i]);
}
}
},
// invokes the listener function
dispatchEvent: function(eventName, eventDetails, component, listener) {
eventDetails.thComponent = component;
// check if there is listener context; if so, execute the listener function using that as the context
if (listener.context) {
listener.listenerFn.apply(listener.context, [ eventDetails ]);
} else {
listener.listenerFn(eventDetails);
}
}
});
// create the global event bus
th.global_event_bus = new th.Bus();
dojo.declare("th.Scene", th.helpers.EventHelpers, {
bus: th.global_event_bus,
css: {},
sheetCount: 0,
currentSheet: 0,
cssLoaded: false,
renderRequested: false,
renderAllowed: true,
constructor: function(canvas) {
this.canvas = canvas;
dojo.connect(window, "resize", dojo.hitch(this, function() {
this.render();
}));
this.root = new th.components.Panel({ id: "root", style: {
// backgroundColor: "pink" // for debugging
} });
this.root.scene = this;
var testCanvas = document.createElement("canvas");
this.scratchContext = testCanvas.getContext("2d");
bespin.util.canvas.fix(this.scratchContext);
dojo.connect(window, "mousedown", dojo.hitch(this, function(e) {
this.wrapEvent(e, this.root);
this.mouseDownComponent = e.thComponent;
th.global_event_bus.fire("mousedown", e, e.thComponent);
}));
dojo.connect(window, "dblclick", dojo.hitch(this, function(e) {
this.wrapEvent(e, this.root);
th.global_event_bus.fire("dblclick", e, e.thComponent);
}));
dojo.connect(window, "click", dojo.hitch(this, function(e) {
this.wrapEvent(e, this.root);
th.global_event_bus.fire("click", e, e.thComponent);
}));
dojo.connect(window, "mousemove", dojo.hitch(this, function(e) {
this.wrapEvent(e, this.root);
th.global_event_bus.fire("mousemove", e, e.thComponent);
if (this.mouseDownComponent) {
this.addComponentXY(e, this.root, this.mouseDownComponent);
th.global_event_bus.fire("mousedrag", e, this.mouseDownComponent);
}
}));
dojo.connect(window, "mouseup", dojo.hitch(this, function(e) {
if (!this.mouseDownComponent) return;
this.addComponentXY(e, this.root, this.mouseDownComponent);
th.global_event_bus.fire("mouseup", e, this.mouseDownComponent);
delete this.mouseDownComponent;
}));
this.parseCSS();
},
render: function(forceRendering) {
if (!this.renderAllowed && !forceRendering) {
return;
}
if (!this.cssLoaded) {
this.renderRequested = true;
return;
}
this.layout();
this.paint();
},
layout: function() {
if (this.root) {
this.root.bounds = { x: 0, y: 0, width: this.canvas.width, height: this.canvas.height };
this.root.layoutTree();
}
},
paint: function(component) {
if (!this.cssLoaded) {
this.renderRequested = true;
return;
}
if (!component) component = this.root;
//if (component === this.root) console.log("root paint");
if (component) {
if (!component.opaque && component.parent) {
return this.paint(component.parent);
}
var ctx = this.canvas.getContext("2d");
bespin.util.canvas.fix(ctx);
ctx.save();
var parent = component.parent;
var child = component;
while (parent) {
ctx.translate(child.bounds.x, child.bounds.y);
child = parent;
parent = parent.parent;
}
ctx.clearRect(0, 0, component.bounds.width, component.bounds.height);
ctx.beginPath();
ctx.rect(0, 0, component.bounds.width, component.bounds.height);
ctx.closePath();
ctx.clip();
component.paint(ctx);
ctx.restore();
}
},
parseCSS: function() {
var links = [];
var s, l = document.getElementsByTagName('link');
for (var i=0; i < l.length; i++){
s = l[i];
if (s.rel.toLowerCase().indexOf('stylesheet') >= 0&&s.href) {
links.push(s.href);
}
}
if (links.length == 0) {
this.cssLoaded = true;
return;
}
this.sheetCount = links.length;
dojo.forEach(links, function(link) {
dojo.xhrGet({
url: link,
load: dojo.hitch(this, function(response) {
this.processCSS(response);
})
});
}, this);
},
processCSS: function(stylesheet) {
this.css = new th.css.CSSParser().parse(stylesheet, this.css);
if (++this.currentSheet == this.sheetCount) {
this.cssLoaded = true;
if (this.renderRequested) {
this.render();
this.renderRequested = false;
}
}
}
});
dojo.declare("th.Border", th.helpers.ComponentHelpers, {
constructor: function(parms) {
if (!parms) parms = {};
this.style = parms.style || {};
this.attributes = parms.attributes || {};
},
getInsets: function() {
return this.emptyInsets();
},
paint: function(ctx) {}
});
dojo.declare("th.Component", th.helpers.ComponentHelpers, {
constructor: function(parms) {
if (!parms) parms = {};
this.bounds = parms.bounds || {};
this.style = parms.style || {};
this.attributes = parms.attributes || {};
this.id = parms.id;
this.border = parms.border;
this.opaque = parms.opaque || true;
this.bus = th.global_event_bus;
},
// used to obtain a throw-away canvas context for performing measurements, etc.; may or may not be the same canvas as that used to draw the component
getScratchContext: function() {
var scene = this.getScene();
if (scene) return scene.scratchContext;
},
getPreferredHeight: function(width) {},
getPreferredWidth: function(height) {},
getInsets: function() {
return (this.border) ? this.border.getInsets() : this.emptyInsets();
},
paint: function(ctx) {
console.log("default component paint");
},
repaint: function() {
this.getScene().paint(this);
}
});
dojo.declare("th.Container", [th.Component, th.helpers.ContainerHelpers], {
constructor: function() {
this.children = [];
},
add: function() {
for (var z = 0; z < arguments.length; z++) {
component = dojo.isArray(arguments[z]) ? arguments[z] : [ arguments[z] ];
this.children = this.children.concat(component);
for (var i = 0; i < component.length; i++) {
component[i].parent = this;
}
}
},
remove: function() {
for (var z = 0; z < arguments.length; z++) {
component = dojo.isArray(arguments[z]) ? arguments[z] : [ arguments[z] ];
for (var i = 0; i < component.length; i++) {
var old_length = this.children.length;
this.children = dojo.filter(this.children, function(item){ return item != component[i]; });
// if the length of the array has changed since I tried to remove the current component, assume it was removed and clear the parent
if (old_length != this.children.length) delete component[i].parent;
}
}
},
replace: function(component, index) {
this.bus.unbind(this.children[index]);
component.parent = this;
this.children[index] = component;
},
paint: function(ctx) {
if (this.shouldPaint()) {
this.paintSelf(ctx);
this.paintChildren(ctx);
}
},
paintSelf: function(ctx) {},
paintChildren: function(ctx) {
for (var i = 0; i < this.children.length; i++ ) {
if (!this.children[i].shouldPaint()) continue;
if (!this.children[i].bounds) {
// console.log("WARNING: child " + i + " (type: " + this.children[i].declaredClass + ", id: " + this.children[i].id + ") of parent with id " + this.id + " of type " + this.declaredClass + " has no bounds and could not be painted");
continue;
}
ctx.save();
try {
ctx.translate(this.children[i].bounds.x, this.children[i].bounds.y);
} catch (error) {
// console.log("WARNING: child " + i + " (type: " + this.children[i].declaredClass + ", id: " + this.children[i].id + ") of parent with id " + this.id + " of type " + this.declaredClass + " has malformed bounds and could not be painted");
// console.log(this.children[i].bounds);
ctx.restore();
continue;
}
try {
if (!this.children[i].style["noClip"]) {
ctx.beginPath();
ctx.rect(0, 0, this.children[i].bounds.width, this.children[i].bounds.height);
ctx.closePath();
ctx.clip();
}
} catch(ex) {
// console.log("Bounds problem");
// console.log(this.children[i].declaredClass);
// console.log(this.children[i].bounds);
}
ctx.save();
this.children[i].paint(ctx);
ctx.restore();
if (this.children[i].style.border) {
this.children[i].style.border.component = this.children[i];
ctx.save();
this.children[i].style.border.paint(ctx);
ctx.restore();
}
ctx.restore();
}
},
// lays out this container and any sub-containers
layoutTree: function() {
this.layout();
for (var i = 0; i < this.children.length; i++) {
if (this.children[i].layoutTree) this.children[i].layoutTree();
}
},
layout: function() {
var d = this.d();
if (this.children.length > 0) {
var totalWidth = this.bounds.width - d.i.w;
var individualWidth = totalWidth / this.children.length;
for (var i = 0; i < this.children.length; i++) {
this.children[i].bounds = { x: (i * individualWidth) + d.i.l, y: d.i.t, width: individualWidth, height: this.bounds.height - d.i.h };
}
}
},
render: function() {
this.layoutTree();
this.repaint();
}
});
dojo.declare("th.Window", null, {
constructor: function(parms) {
parms = parms || {};
this.containerId = parms.containerId || false;
this.width = parms.width || 200;
this.height = parms.height || 300;
this.title = parms.title || 'NO TITLE GIVEN!';
this.y = parms.top || 50;
this.x = parms.left || 50;
this.isVisible = false;
this.closeOnClickOutside = !!parms.closeOnClickOutside;
// some things must be given
if(!parms.containerId) {
console.error('The "containerId" must be given!');
return;
}
if (dojo.byId(this.containerId)) {
console.error('There is already a element with the id "'+this.containerId+'"!');
return;
}
if (!parms.userPanel) {
console.error('The "userPanel" must be given!');
return;
}
// insert the HTML to the document for the new window and create the scene
dojo.byId('popup_insert_point').innerHTML += '<div id="'+this.containerId+'" class="popupWindow"></div>';
this.container = dojo.byId(this.containerId);
dojo.attr(this.container, { width: this.width, height: this.height, tabindex: '-1' });
this.container['moz-opaque'] = 'true';
this.container.innerHTML = "<canvas id='"+this.containerId+"_canvas'></canvas>";
this.canvas = dojo.byId(this.containerId + '_canvas');
dojo.attr(this.canvas, { width: this.width, height: this.height, tabindex: '-1' });
this.scene = new th.Scene(this.canvas);
this.windowPanel = new th.components.WindowPanel(parms.title, parms.userPanel);
this.windowPanel.windowBar.parentWindow = this;
this.scene.root.add(this.windowPanel);
this.move(this.x, this.y);
// add some listeners for closing the window
// close the window, if the user clicks outside the window
dojo.connect(window, "mousedown", dojo.hitch(this, function(e) {
if (!this.isVisible || !this.closeOnClickOutside) return;
var d = dojo.coords(this.container);
if (e.clientX < d.l || e.clientX > (d.l + d.w) || e.clientY < d.t || e.clientY > (d.t + d.h)) {
this.toggle();
} else {
dojo.stopEvent(e);
}
}));
// close the window, if the user pressed ESCAPE
dojo.connect(window, "keydown", dojo.hitch(this, function(e) {
if (!this.isVisible) return;
if(e.keyCode == bespin.util.keys.Key.ESCAPE) {
this.toggle();
dojo.stopEvent(e);
}
}));
},
toggle: function() {
this.isVisible = !this.isVisible;
this.scene.bus.fire("toggle", {}, this);
if (this.isVisible) {
this.container.style.display = 'block';
this.layoutAndRender();
} else {
this.container.style.display = 'none';
}
},
layoutAndRender: function() {
this.scene.layout();
this.scene.render();
},
centerUp: function() {
this.move(Math.round((window.innerWidth - this.width) * 0.5), Math.round((window.innerHeight - this.height) * 0.25));
},
center: function() {
this.move(Math.round((window.innerWidth - this.width) * 0.5), Math.round((window.innerHeight - this.height) * 0.5));
},
move: function(x, y) {
this.y = y;
this.x = x;
this.container.style.top = y + 'px';
this.container.style.left = x + 'px';
},
getPosition: function() {
return { x: this.x, y: this.y };
}
});