| // ***** 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): |
| // |
| // ***** END LICENSE BLOCK ***** |
| // |
| |
| dojo.provide("th.components"); |
| |
| dojo.declare("th.components.Button", th.Component, { |
| paint: function(ctx) { |
| var d = this.d(); |
| |
| if (this.style.topImage && this.style.middleImage && this.style.bottomImage) { |
| if (d.b.h >= this.style.topImage.height + this.style.bottomImage.height) { |
| ctx.drawImage(this.style.topImage, 0, 0); |
| if (d.b.h > this.style.topImage.height + this.style.bottomImage.height) { |
| ctx.drawImage(this.style.middleImage, 0, this.style.topImage.height, this.style.middleImage.width, d.b.h - this.style.topImage.height - this.style.bottomImage.height); |
| } |
| ctx.drawImage(this.style.bottomImage, 0, d.b.h - this.style.bottomImage.height); |
| } |
| } else if (this.style.backgroundImage) { |
| ctx.drawImage(this.style.backgroundImage, 0, 0); |
| } else { |
| ctx.fillStyle = "red"; |
| ctx.fillRect(0, 0, d.b.w, d.b.h); |
| } |
| } |
| }); |
| |
| dojo.declare("th.components.Scrollbar", th.Container, { |
| constructor: function(parms) { |
| if (!parms) parms = {}; |
| this.orientation = parms.orientation || th.VERTICAL; |
| this.value = parms.value || 0; |
| this.min = parms.min || 0; |
| this.max = parms.max || 100; |
| this.extent = parms.extent || 0.1; |
| this.increment = parms.increment || 2; |
| this.style = parms.style || {}; |
| |
| this.up = new th.components.Button(); |
| this.down = new th.components.Button(); |
| this.bar = new th.components.Button(); |
| this.add([ this.up, this.down, this.bar ]); |
| |
| this.bus.bind("click", this.up, this.scrollup, this); |
| this.bus.bind("click", this.down, this.scrolldown, this); |
| this.bus.bind("mousedrag", this.bar, this.onmousedrag, this); |
| this.bus.bind("mouseup", this.bar, this.onmouseup, this); |
| }, |
| |
| loadImages: function(path, name) { |
| function loadImg(url) { |
| var img = new Image(); |
| img.src = url; |
| return img; |
| } |
| // getting the images for the scrollbar |
| this.style.scrollUpArrow = loadImg(path + name + '_up_arrow.png'); |
| this.style.scrollHandleTopImage = loadImg(path + name + '_top.png'); |
| this.style.scrollHandleMiddleImage = loadImg(path + name + '_middle.png'); |
| this.style.scrollHandleBottomImage = loadImg(path + name + '_bottom.png'); |
| this.style.scrollDownArrow = loadImg(path + name + '_down_arrow.png'); |
| }, |
| |
| onmousedrag: function(e) { |
| var currentPosition = (this.orientation == th.VERTICAL) ? e.clientY : e.clientX; |
| |
| if (this.dragstart_value == undefined) { |
| this.dragstart_value = this.value; |
| this.dragstart_mouse = currentPosition; |
| return; |
| } |
| |
| var diff = currentPosition - this.dragstart_mouse; // difference in pixels; needs to be translated to a difference in value |
| |
| var pixel_range = this.bounds.height - this.up.bounds.height - this.down.bounds.height - this.bar.bounds.height; // total number of pixels that map to the value range |
| |
| var pixel_to_value_ratio = (this.max - this.min) / pixel_range; |
| |
| this.value = this.dragstart_value + Math.floor(diff * pixel_to_value_ratio); |
| if (this.value < this.min) this.value = this.min; |
| if (this.value > this.max) this.value = this.max; |
| if (this.scrollable) this.scrollable.scrollTop = this.value; |
| this.render(); |
| if (this.scrollable) this.scrollable.repaint(); |
| }, |
| |
| onmouseup: function(e) { |
| delete this.dragstart_value; |
| delete this.dragstart_mouse; |
| }, |
| |
| scrollup: function(e) { |
| if (this.value > this.min) { |
| this.value = Math.max(this.min, this.value - this.increment); |
| if (this.scrollable) this.scrollable.scrollTop = this.value; |
| this.render(); |
| if (this.scrollable) this.scrollable.repaint(); |
| } |
| }, |
| |
| scrolldown: function(e) { |
| if (this.value < this.max) { |
| this.value = Math.min(this.max, this.value + this.increment); |
| if (this.scrollable) this.scrollable.scrollTop = this.value; |
| this.render(); |
| if (this.scrollable) this.scrollable.repaint(); |
| } |
| }, |
| |
| layout: function() { |
| var d = this.d(); |
| |
| // check if there's a scrollable attached; if so, refresh state |
| if (this.scrollable) { |
| var view_height = this.scrollable.bounds.height; |
| var scrollable_info = this.scrollable.getScrollInfo(); |
| this.min = 0; |
| this.max = scrollable_info.scrollHeight - view_height; |
| this.value = scrollable_info.scrollTop; |
| this.extent = (scrollable_info.scrollHeight - view_height) / scrollable_info.scrollHeight; |
| } |
| |
| // if the maximum value is less than the minimum, we're in an invalid state and won't paint anything |
| if (this.max < this.min) { |
| for (var i = 0; i < this.children.length; i++) delete this.children[i].bounds; |
| return; |
| } |
| |
| if (this.orientation == th.VERTICAL) { |
| var w = d.b.iw; |
| var h = 12; |
| this.up.bounds = { x: d.i.l + 1, y: d.i.t, width: w, height: h }; |
| this.down.bounds = { x: d.i.l + 1, y: d.b.ih - h, width: w, height: h }; |
| |
| var scroll_track_height = d.b.ih - this.up.bounds.height - this.down.bounds.height; |
| |
| var extent_length = Math.min(Math.floor(scroll_track_height - (this.extent * scroll_track_height), d.b.ih - this.up.bounds.height - this.down.bounds.height)); |
| var extent_top = Math.floor(this.up.bounds.height + Math.min( (this.value / (this.max - this.min)) * (scroll_track_height - extent_length) )); |
| this.bar.bounds = { x: d.i.l + 1, y: extent_top, width: d.b.iw, height: extent_length }; |
| } else { |
| |
| } |
| }, |
| |
| paint: function(ctx) { |
| if (this.max < 0) return; |
| |
| // paint the track |
| if (this.style.scrollTopImage) ctx.drawImage(this.style.scrollTopImage, 1, this.up.bounds.height); |
| if (this.style.scrollMiddleImage) ctx.drawImage(this.style.scrollMiddleImage, 1, this.up.bounds.height + this.style.scrollTopImage.height, this.style.scrollMiddleImage.width, this.down.bounds.y - this.down.bounds.height - (this.up.bounds.x - this.up.bounds.height)); |
| if (this.style.scrollBottomImage) ctx.drawImage(this.style.scrollBottomImage, 1, this.down.bounds.y - this.style.scrollBottomImage.height); |
| |
| // propagate the styles to the children if not already there |
| if (this.style.scrollHandleTopImage && !this.bar.style.topImage) { |
| this.bar.style.topImage = this.style.scrollHandleTopImage; |
| this.bar.style.middleImage = this.style.scrollHandleMiddleImage; |
| this.bar.style.bottomImage = this.style.scrollHandleBottomImage; |
| this.up.style.backgroundImage = this.style.scrollUpArrow; |
| this.down.style.backgroundImage = this.style.scrollDownArrow; |
| } |
| |
| this.inherited(arguments); |
| } |
| }); |
| |
| dojo.declare("th.components.Panel", th.Container, { |
| paintSelf: function(ctx) { |
| if (this.style.backgroundColor) { |
| ctx.fillStyle = this.style.backgroundColor; |
| |
| var x = 0; |
| var y = 0; |
| var w = this.bounds.width; |
| var h = this.bounds.height; |
| |
| ctx.fillRect(x, y, w, h); |
| } |
| } |
| }); |
| |
| |
| dojo.declare("th.components.ResizeNib", th.Component, { |
| constructor: function(parms) { |
| this.bus.bind("mousedown", this, this.onmousedown, this); |
| this.bus.bind("mouseup", this, this.onmouseup, this); |
| this.bus.bind("mousedrag", this, this.onmousedrag, this); |
| }, |
| |
| onmousedown: function(e) { |
| this.startPos = { x: e.clientX, y: e.clientY}; |
| }, |
| |
| onmousedrag: function(e) { |
| if (this.startPos) { |
| if (!this.firedDragStart) { |
| this.bus.fire("dragstart", this.startPos, this); |
| this.firedDragStart = true; |
| } |
| |
| this.bus.fire("drag", { startPos: this.startPos, currentPos: { x: e.clientX, y: e.clientY } }, this); |
| } |
| }, |
| |
| onmouseup: function(e) { |
| if (this.startPos && this.firedDragStart) { |
| this.bus.fire("dragstop", { startPos: this.startPos, currentPos: { x: e.clientX, y: e.clientY } }, this); |
| delete this.firedDragStart; |
| } |
| delete this.startPos; |
| }, |
| |
| paint: function(ctx) { |
| var d = this.d(); |
| |
| if (this.attributes.orientation == th.VERTICAL) { |
| var bw = 7; |
| var x = Math.floor((d.b.w / 2) - (bw / 2)); |
| var y = 7; |
| |
| ctx.fillStyle = "rgb(185, 180, 158)"; |
| for (var i = 0; i < 3; i++) { |
| ctx.fillRect(x, y, bw, 1); |
| y += 3; |
| } |
| |
| y = 8; |
| ctx.fillStyle = "rgb(10, 10, 8)"; |
| for (var i = 0; i < 3; i++) { |
| ctx.fillRect(x, y, bw, 1); |
| y += 3; |
| } |
| } else { |
| var bh = 7; |
| |
| var dw = 8; // width of the bar area |
| var dh = bh + 2; // height of the bar area |
| |
| var x = Math.floor(d.b.w / 2 - (dw / 2)); |
| var y = Math.floor(d.b.h / 2 - (dh / 2)); |
| |
| // lay down the shadowy bits |
| var cx = x; |
| ctx.fillStyle = "rgba(0, 0, 0, 0.1)"; |
| for (var i = 0; i < 3; i++) { |
| ctx.fillRect(cx, y, 1, dh); |
| cx += 3; |
| } |
| |
| // lay down the black shadow |
| var cx = x + 1; |
| ctx.fillStyle = "black"; |
| for (var i = 0; i < 3; i++) { |
| ctx.fillRect(cx, y + dh - 1, 1, 1); |
| cx += 3; |
| } |
| |
| // draw the bars |
| var cx = x + 1; |
| ctx.fillStyle = "rgb(183, 180, 160)"; |
| for (var i = 0; i < 3; i++) { |
| ctx.fillRect(cx, y + 1, 1, bh); |
| cx += 3; |
| } |
| } |
| } |
| }); |
| |
| |
| |
| /* |
| A "splitter" that visually demarcates areas of an interface. Can also have some "nibs" on its ends to facilitate resizing. |
| Provides "dragstart", "drag", and "dragstop" events that are fired when a nib is dragged. Orientation is in terms of a container and |
| is confusing; HORIZONTAL means the splitter is actually displayed taller than wide--what might be called vertically, and similarly |
| VERTICAL means the splitter is wider than it is tall, i.e., horizontally. This is because the *container* is laid out such that |
| different regions are stacked horizontally or vertically, and the splitter demarcates those areas. |
| |
| This bit of confusion was deemed better than having the orientation for a hierarchy of components be different but contributing to the |
| same end. |
| |
| Note also that this component uses getPreferredHeight() and getPreferredWidth() differently than most; only one of the methods is |
| valid for a particular orientation. I.e., when in HORIZONTAL orientation, getPreferredWidth() should be used and getPreferredHeight() |
| ignored. |
| |
| */ |
| dojo.declare("th.components.Splitter", th.Container, { |
| constructor: function(parms) { |
| this.topNib = new th.components.ResizeNib({ attributes: { orientation: this.attributes.orientation } }); |
| this.bottomNib = new th.components.ResizeNib({ attributes: { orientation: this.attributes.orientation } }); |
| this.add(this.topNib, this.bottomNib); |
| |
| this.label = parms.label; |
| if (this.label) this.add(this.label); |
| |
| this.scrollbar = parms.scrollbar; |
| if (this.scrollbar) this.add(this.scrollbar); |
| |
| this.bus.bind("drag", [ this.topNib, this.bottomNib ], this.ondrag, this); |
| this.bus.bind("dragstart", [ this.topNib, this.bottomNib ], this.ondragstart, this); |
| this.bus.bind("dragstop", [ this.topNib, this.bottomNib ], this.ondragstop, this); |
| }, |
| |
| ondrag: function(e) { |
| this.bus.fire("drag", e, this); |
| }, |
| |
| ondragstart: function(e) { |
| this.bus.fire("dragstart", e, this); |
| }, |
| |
| ondragstop: function(e) { |
| this.bus.fire("dragstop", e, this); |
| }, |
| |
| getPreferredHeight: function(width) { |
| return 20; |
| }, |
| |
| getPreferredWidth: function(height) { |
| return 16; |
| }, |
| |
| layout: function() { |
| var d = this.d(); |
| |
| // if the orientation isn't explicitly set, guess it by examining the ratio |
| if (!this.attributes.orientation) this.attributes.orientation = (this.bounds.height > this.bounds.width) ? th.HORIZONTAL : th.VERTICAL; |
| |
| if (this.attributes.orientation == th.HORIZONTAL) { |
| this.topNib.bounds = { x: 0, y: 0, height: d.b.w, width: d.b.w }; |
| this.bottomNib.bounds = { x: 0, y: this.bounds.height - d.b.w, height: d.b.w, width: d.b.w }; |
| |
| if (this.scrollbar && this.scrollbar.shouldLayout()) { |
| this.scrollbar.bounds = { x: 0, y: this.topNib.bounds.height, height: d.b.h - (this.topNib.bounds.height * 2), width: d.b.w }; |
| } |
| } else { |
| this.topNib.bounds = { x: 0, y: 0, height: d.b.h, width: d.b.h }; |
| this.bottomNib.bounds = { x: d.b.w - d.b.h, y: 0, height: d.b.h, width: d.b.h }; |
| |
| if (this.label) { |
| this.label.bounds = { x: this.topNib.bounds.x + this.topNib.bounds.width, y: 0, height: d.b.h, width: d.b.w - (d.b.h * 2) }; |
| } |
| } |
| }, |
| |
| paintSelf: function(ctx) { |
| var d = this.d(); |
| if (this.attributes.orientation == th.VERTICAL) { |
| ctx.fillStyle = "rgb(73, 72, 66)"; |
| ctx.fillRect(0, 0, d.b.w, 1); |
| ctx.fillStyle = "black"; |
| ctx.fillRect(0, d.b.h - 1, d.b.w, 1); |
| |
| var gradient = ctx.createLinearGradient(0, 1, 0, d.b.h - 1); |
| gradient.addColorStop(0, "rgb(50, 48, 42)"); |
| gradient.addColorStop(1, "rgb(22, 22, 19)"); |
| ctx.fillStyle = gradient; |
| ctx.fillRect(0, 1, d.b.w, d.b.h - 2); |
| } else { |
| ctx.fillStyle = "rgb(105, 105, 99)"; |
| ctx.fillRect(0, 0, 1, d.b.h); |
| ctx.fillStyle = "black"; |
| ctx.fillRect(d.b.w - 1, 0, 1, d.b.h); |
| |
| var gradient = ctx.createLinearGradient(1, 0, d.b.w - 2, 0); |
| gradient.addColorStop(0, "rgb(56, 55, 49)"); |
| gradient.addColorStop(1, "rgb(62, 61, 55)"); |
| ctx.fillStyle = gradient; |
| ctx.fillRect(1, 0, d.b.w - 2, d.b.h); |
| } |
| } |
| }); |
| |
| dojo.declare("th.components.SplitPanelContainer", th.components.Panel, { |
| constructor: function(parms) { |
| this.splitter = new th.components.Splitter({ attributes: { orientation: this.attributes.orientation }, label: parms.label }); |
| }, |
| |
| getContents: function() { |
| var childrenWithoutSplitter = dojo.filter(this.children, |
| dojo.hitch(this, function(item){ return item != this.splitter; }) |
| ); |
| if (childrenWithoutSplitter.length > 0) return childrenWithoutSplitter[0]; |
| }, |
| |
| layout: function() { |
| var childrenWithoutSplitter = dojo.filter(this.children, |
| dojo.hitch(this, function(item){ return item != this.splitter; }) |
| ); |
| if (this.children.length == childrenWithoutSplitter.length) this.add(this.splitter); |
| |
| var slength = (this.attributes.orientation == th.HORIZONTAL) ? |
| this.splitter.getPreferredWidth(this.bounds.height) : |
| this.splitter.getPreferredHeight(this.bounds.width); |
| if (this.splitter.shouldLayout()) { |
| if (this.attributes.orientation == th.HORIZONTAL) { |
| this.splitter.bounds = { x: this.bounds.width - slength, y: 0, height: this.bounds.height, width: slength }; |
| } else { |
| this.splitter.bounds = { x: 0, y: this.bounds.height - slength, height: slength, width: this.bounds.width }; |
| } |
| } else { |
| slength = 0; |
| } |
| |
| // only the first non-splitter child is laid out |
| if (childrenWithoutSplitter.length > 0) { |
| if (this.attributes.orientation == th.HORIZONTAL) { |
| childrenWithoutSplitter[0].bounds = { x: 0, y: 0, height: this.bounds.height, width: this.bounds.width - slength }; |
| } else { |
| childrenWithoutSplitter[0].bounds = { x: 0, y: 0, height: this.bounds.height - slength, width: this.bounds.width }; |
| } |
| } |
| } |
| }); |
| |
| /* |
| A component that allocates all visible space to two or more nested regions. |
| */ |
| dojo.declare("th.components.SplitPanel", th.components.Panel, { |
| constructor: function(parms) { |
| if (!this.attributes.orientation) this.attributes.orientation = th.HORIZONTAL; |
| |
| if (!this.attributes.regions) this.attributes.regions = [{},{}]; |
| }, |
| |
| ondragstart: function(e) { |
| var container = e.thComponent.parent; // splitter -> splitpanecontainer |
| container.region.startSize = container.region.size; |
| }, |
| |
| ondrag: function(e) { |
| var container = e.thComponent.parent; // splitter -> splitpanecontainer |
| |
| var delta = (this.attributes.orientation == th.HORIZONTAL) ? e.currentPos.x - e.startPos.x : e.currentPos.y - e.startPos.y; |
| |
| container.region.size = container.region.startSize + delta; |
| this.render(); |
| }, |
| |
| ondragstop: function(e) { |
| var container = e.thComponent.parent; // splitter -> splitpanecontainer |
| delete container.region.startSize; |
| }, |
| |
| layout: function() { |
| this.remove(this.children); // remove any of the existing region panels |
| |
| /* |
| iterate through each region, performing a couple of tasks: |
| - create a container for each region if it doesn't already have one |
| - put the value of the contents property of region into the container if necessary |
| - hide the splitter on the last region |
| */ |
| var i; |
| for (i = 0; i < this.attributes.regions.length; i++) { |
| var region = this.attributes.regions[i]; |
| if (!region.container) { |
| region.container = new th.components.SplitPanelContainer({ attributes: { orientation: this.attributes.orientation }, label: region.label }); |
| |
| region.container.region = region; // give the container a reference back to the region |
| |
| // capture the start size of the region when the nib's drag starts |
| this.bus.bind("dragstart", region.container.splitter, this.ondragstart, this); |
| this.bus.bind("drag", region.container.splitter, this.ondrag, this); |
| this.bus.bind("dragstop", region.container.splitter, this.ondragstop, this); |
| } |
| |
| // update the content panel for the split panel container |
| if (region.contents && (region.contents != region.container.getContents())) { |
| region.container.removeAll(); |
| region.container.add(region.contents); |
| } |
| |
| // make the last container's splitter invisible |
| if (i == this.attributes.regions.length - 1) region.container.splitter.style.display = "none"; |
| |
| this.add(region.container); |
| } |
| |
| var containerSize = (this.attributes.orientation == th.HORIZONTAL) ? this.bounds.width : this.bounds.height; |
| |
| // size the regions |
| var totalSize = 0; |
| for (i = 0; i < this.attributes.regions.length; i++) { |
| var r = this.attributes.regions[i]; |
| |
| if (!r.size) { |
| r.size = (this.attributes.defaultSize || (100 / this.attributes.regions.length) + "%"); |
| } |
| |
| if (th.helpers.isPercentage(r.size)) { |
| // percentage lengths are allowed, but will be immediately converted to pixels |
| r.size = Math.floor((parseInt(r.size) / 100) * containerSize); |
| } |
| |
| // enforce a minimum width |
| if (r.size < 30) r.size = 30; |
| |
| totalSize += r.size; |
| } |
| if (totalSize > containerSize) { // if the regions are bigger than the split pane size, shrink 'em, right-to-left |
| var diff = totalSize - containerSize; |
| for (i = this.attributes.regions.length - 1; i >= 0; i--) { |
| var r = this.attributes.regions[i]; |
| |
| var originalSize = r.size; |
| r.size -= diff; |
| if (r.size < 30) r.size = 30; |
| diff -= (originalSize - r.size); |
| |
| if (diff <= 0) break; |
| } |
| } else if (totalSize < containerSize) { // if the regions are smaller, grow 'em, all in the last one |
| this.attributes.regions[this.attributes.regions.length - 1].size += (containerSize - totalSize); |
| } |
| |
| var startPx = 0; |
| for (i = 0; i < this.attributes.regions.length; i++) { |
| var region = this.attributes.regions[i]; |
| if (this.attributes.orientation == th.HORIZONTAL) { |
| region.container.bounds = { x: startPx, y: 0, width: region.size, height: this.bounds.height }; |
| } else { |
| region.container.bounds = { x: 0, y: startPx, width: this.bounds.width, height: region.size }; |
| } |
| startPx += region.size; |
| |
| } |
| } |
| }); |
| |
| dojo.declare("th.components.Label", th.components.Panel, { |
| constructor: function(parms) { |
| if (!parms) parms = {}; |
| if (!this.border) this.border = new th.borders.EmptyBorder({ insets: { left: 5, right: 5, top: 2, bottom: 2 }}); |
| this.attributes.text = parms.text || ""; |
| if (!this.style.font) this.style.font = "12pt Arial"; |
| if (!this.style.color) this.style.color = "black"; |
| }, |
| |
| styleContext: function(ctx) { |
| if (!ctx) return; |
| |
| ctx.font = this.style.font; |
| ctx.fillStyle = this.style.color; |
| |
| return ctx; |
| }, |
| |
| getPreferredWidth: function(height) { |
| var ctx = this.styleContext(this.parent.getScratchContext()); |
| |
| // the +2 is to compensate for anti-aliasing on Windows, which isn't taken into account in measurements; this fudge factor should eventually become platform-specific |
| var w = ctx.measureText(this.attributes.text).width + 2; |
| return w + this.getInsets().left + this.getInsets().right; |
| }, |
| |
| getPreferredHeight: function(width) { |
| var ctx = this.styleContext(this.parent.getScratchContext()); |
| var h = Math.floor(ctx.measureText(this.attributes.text).ascent * 1.5); // multiplying by 2 to fake a descent and leading |
| return h + this.getInsets().top + this.getInsets().bottom; |
| }, |
| |
| paint: function(ctx) { |
| var d = this.d(); |
| |
| if (this.style.backgroundColor) this.inherited(arguments); |
| |
| this.styleContext(ctx); |
| |
| var textMetrics = ctx.measureText(this.attributes.text); |
| |
| var textToRender = this.attributes.text; |
| var lastLength = textToRender.length - 2; |
| while (textMetrics.width > (d.b.w - d.i.w)) { |
| if (lastLength == 0) { |
| textToRender = "..."; |
| break; |
| } |
| |
| var left = Math.floor(lastLength / 2); |
| var right = left + (lastLength % 2); |
| textToRender = this.attributes.text.substring(0, left) + "..." + this.attributes.text.substring(this.attributes.text.length - right); |
| textMetrics = ctx.measureText(textToRender); |
| |
| lastLength -= 1; |
| } |
| |
| var y = this.getInsets().top + textMetrics.ascent; |
| if (dojo.isWebKit) y += 1; // strings are one pixel too high in Safari 4 and Webkit nightly |
| |
| ctx.fillText(textToRender, this.getInsets().left, y); |
| } |
| }); |
| |
| dojo.declare("th.components.ExpandingInfoPanel", th.components.Panel, { |
| getMinimumRowHeight: function() { |
| return 40; |
| }, |
| |
| getMinimumColumnWidth: function() { |
| |
| }, |
| |
| layout: function() { |
| if (this.children.length == 0) return; |
| |
| var d = this.d(); |
| |
| |
| var rows = Math.floor(Math.sqrt(this.children.length)); |
| var height = Math.floor(d.b.h / rows); |
| while (height < this.getMinimumRowHeight() && rows > 1) { |
| rows--; |
| height = Math.floor(d.b.h / rows); |
| } |
| |
| |
| var perRow = Math.floor(this.children.length / rows); |
| var remainder = this.children.length % rows; |
| |
| // TODO: verify a minimum height (and perhaps width) |
| |
| var currentChild = 0; |
| var heightRemainder = d.b.h % rows; |
| |
| var currentY = 0; |
| for (var i = 0; i < rows; i++) { |
| var h = (i == rows - 1) ? height + heightRemainder : height; |
| |
| var cols = (remainder > 0) ? perRow + 1 : perRow; |
| remainder--; |
| |
| var width = Math.floor(d.b.w / cols); |
| var widthRemainder = d.b.w % cols; |
| |
| var currentX = 0; |
| for (var z = 0; z < cols; z++) { |
| var w = (z == cols - 1) ? width + widthRemainder : width; |
| this.children[currentChild++].bounds = { x: currentX, y: currentY, width: w, height: h }; |
| currentX += w; |
| } |
| currentY += h; |
| } |
| } |
| }); |
| |
| dojo.declare("th.components.List", th.Container, { |
| constructor: function(parms) { |
| if (!parms) parms = {}; |
| |
| this.items = parms.items || []; |
| |
| this.scrollTop = 0; |
| |
| this.allowDeselection = parms.allowDeselection || false; |
| |
| this.bus.bind("mousedown", this, this.onmousedown, this); |
| |
| this.renderer = new th.components.Label({ style: { border: new th.borders.EmptyBorder({ size: 3 }) }}); |
| |
| if (parms.topLabel) { |
| this.label = parms.topLabel; |
| this.label.height = 16; |
| } |
| }, |
| |
| onmousedown: function(e) { |
| var item = this.getItemForPosition({ x: e.componentX, y: e.componentY }); |
| if (item != this.selected) { |
| if (item) { |
| this.selected = item; |
| this.bus.fire("itemselected", { container: this, item: this.selected }, this); |
| this.repaint(); |
| } else if(this.allowDeselection) { |
| delete this.selected; |
| } |
| } |
| }, |
| |
| // be carefull! This does NOT fire the "itemselected" event!!! |
| selectItemByText: function(text) { |
| if (this.items.length == 0) return false; |
| var item = null; |
| if (dojo.isObject(this.items[0])) { |
| for(var x = 0; x < this.items.length; x++) { |
| if(this.items[x].name == text) { |
| item = this.items[x]; |
| break; |
| } |
| } |
| if (item == null) return false; |
| } else { |
| if(this.items.indexOf(text) == -1) return false; |
| item = this.items[this.items.indexOf(text)]; |
| } |
| |
| if (this.selected != item) { |
| this.selected = item; |
| this.repaint(); |
| } |
| |
| return true; |
| }, |
| |
| moveSelectionUp: function() { |
| if (!this.selected || this.items.length == 0) return; |
| |
| var x = 0; |
| while (this.items[x] != this.selected) { |
| x ++; |
| } |
| |
| if (x != 0) { |
| this.selected = this.items[x - 1]; |
| this.bus.fire("itemselected", { container: this, item: this.selected }, this); |
| this.repaint(); |
| } |
| }, |
| |
| moveSelectionDown: function() { |
| if (!this.selected || this.items.length == 0) return; |
| |
| var x = 0; |
| while (this.items[x] != this.selected) { |
| x ++; |
| } |
| |
| if (x != this.items.length - 1) { |
| this.selected = this.items[x + 1]; |
| this.bus.fire("itemselected", { container: this, item: this.selected }, this); |
| this.repaint(); |
| } |
| }, |
| |
| getItemForPosition: function(pos) { |
| pos.y += this.scrollTop - (this.label ? this.label.height : 0); |
| var y = this.getInsets().top; |
| for (var i = 0; i < this.items.length; i++) { |
| var h = this.heights[i]; |
| if (pos.y >= y && pos.y <= y + h) return this.items[i]; |
| y += h; |
| } |
| }, |
| |
| getRenderer: function(rctx) { |
| this.renderer.attributes.text = rctx.item.toString(); |
| this.renderer.style.font = this.style.font; |
| this.renderer.style.color = this.style.color; |
| this.renderer.selected = rctx.selected; |
| this.renderer.item = rctx.item; |
| return this.renderer; |
| }, |
| |
| getRenderContext: function(item, row) { |
| return { item: item, even: row % 2 == 0, selected: this.selected == item }; |
| }, |
| |
| getRowHeight: function() { |
| if (!this.rowHeight) { |
| var d = this.d(); |
| var firstItem = (this.items.length > 0) ? this.items[0] : undefined; |
| if (firstItem) { |
| var renderer = this.getRenderer(this.getRenderContext(firstItem, 0)); |
| this.add(renderer); |
| this.rowHeight = renderer.getPreferredHeight(d.b.w - d.i.w); |
| this.remove(renderer); |
| } |
| } |
| return this.rowHeight || 0; |
| }, |
| |
| getScrollInfo: function() { |
| return { scrollTop: this.scrollTop, scrollHeight: this.getRowHeight() * this.items.length } |
| }, |
| |
| paint: function(ctx) { |
| var d = this.d(); |
| |
| var paintHeight = Math.max(this.getScrollInfo().scrollHeight, d.b.h); |
| var scrollInfo = this.getScrollInfo(); |
| |
| ctx.save(); |
| |
| if (this.label) { |
| var prefHeight = this.label.height; |
| this.label.bounds = { y: y, x: d.i.l, height: prefHeight, width: d.b.w }; |
| this.label.paint(ctx); |
| d.i.t = prefHeight; |
| } |
| |
| ctx.translate(0, -this.scrollTop); |
| |
| try { |
| if (this.style.backgroundColor) { |
| ctx.fillStyle = this.style.backgroundColor; |
| ctx.fillRect(0, d.i.t, d.b.w, paintHeight); |
| } |
| |
| if (this.style.backgroundColorOdd) { |
| var rowHeight = this.rowHeight; |
| if (!rowHeight) { |
| var firstItem = (this.items.length > 0) ? this.items[0] : undefined; |
| if (firstItem) { |
| var renderer = this.getRenderer(this.getRenderContext(firstItem, 0)); |
| this.add(renderer); |
| rowHeight = renderer.getPreferredHeight(d.b.w - d.i.w); |
| this.remove(renderer); |
| } |
| } |
| if (rowHeight) { |
| var y = d.i.t + rowHeight; |
| ctx.fillStyle = this.style.backgroundColorOdd; |
| while (y < paintHeight) { |
| ctx.fillRect(d.i.l, y, d.b.w - d.i.w, rowHeight); |
| y += rowHeight * 2; |
| } |
| } |
| } |
| |
| if (this.items.length == 0) return; |
| |
| if (!this.renderer) { |
| console.log("No renderer for List of type " + this.declaredClass + " with id " + this.id + "; cannot paint contents"); |
| return; |
| } |
| |
| |
| this.heights = []; |
| var y = d.i.t; |
| for (var i = 0; i < this.items.length; i++) { |
| var stamp = this.getRenderer(this.getRenderContext(this.items[i], i)); |
| if (!stamp) break; |
| |
| this.add(stamp); |
| |
| var w = d.b.w - d.i.w; |
| var h = (this.rowHeight) ? this.rowHeight : stamp.getPreferredHeight(w); |
| this.heights.push(h); |
| stamp.bounds = { x: 0, y: 0, height: h, width: w }; |
| |
| ctx.save(); |
| ctx.translate(d.i.l, y); |
| ctx.beginPath(); |
| ctx.rect(0, 0, w, h); |
| ctx.closePath(); |
| ctx.clip(); |
| |
| stamp.paint(ctx); |
| |
| ctx.restore(); |
| |
| this.remove(stamp); |
| |
| y+= h; |
| } |
| } finally { |
| ctx.restore(); |
| } |
| } |
| }); |
| |
| dojo.declare("th.components.HorizontalTree", th.Container, { |
| constructor: function(parms) { |
| if (!parms) parms = {}; |
| if (!this.style.defaultSize) this.style.defaultSize = 150; |
| |
| this.attributes.orientation = th.HORIZONTAL; |
| |
| this.lists = []; |
| this.splitters = []; |
| this.listWidths = []; |
| }, |
| |
| setData: function(data) { |
| for (var i = 0; i < this.lists.length; i++) { |
| this.remove(this.lists[i]); |
| this.remove(this.splitters[i]); |
| this.bus.unbind(this.lists[i]); |
| this.bus.unbind(this.splitters[i]); |
| } |
| this.lists = []; |
| this.splitters = []; |
| |
| this.data = data; |
| this.showChildren(null, data); |
| }, |
| |
| ondragstart: function(e) { |
| var splitterIndex = this.splitters.indexOf(e.thComponent); |
| this.startSize = this.listWidths[splitterIndex]; |
| }, |
| |
| ondrag: function(e) { |
| var splitterIndex = this.splitters.indexOf(e.thComponent); |
| var delta = (this.attributes.orientation == th.HORIZONTAL) ? e.currentPos.x - e.startPos.x : e.currentPos.y - e.startPos.y; |
| this.listWidths[splitterIndex] = this.startSize + delta; |
| this.render(); |
| }, |
| |
| ondragstop: function(e) { |
| delete this.startSize; |
| }, |
| |
| updateData: function(parent, contents) { |
| parent.contents = contents; |
| if (this.getSelectedItem() == parent) { |
| this.showChildren(parent, parent.contents); |
| } |
| }, |
| |
| replaceList: function(index, contents) { |
| this.lists[index].items = contents; |
| delete this.lists[index].selected; |
| this.render(); |
| }, |
| |
| removeListsFrom: function(index) { |
| for (var x = index; x < this.lists.length; x++) |
| { |
| this.bus.unbind(this.lists[x]); |
| this.bus.unbind(this.splitters[x]); |
| |
| this.remove(this.lists[x]); |
| this.remove(this.splitters[x]); |
| } |
| |
| this.lists = this.lists.slice(0, index); |
| this.splitters = this.splitters.slice(0, index); |
| }, |
| |
| showChildren: function(newItem, children) { |
| if (this.details) { |
| this.remove(this.details); |
| delete this.details; |
| } |
| |
| if (!dojo.isArray(children)) { |
| // if it's not an array, assume it's a function that will load the children |
| children(this.getSelectedPath(), this); |
| return; |
| } |
| |
| if (!children || children.length == 0) return; |
| var list = this.createList(children); |
| list.id = "list " + (this.lists.length + 1); |
| |
| this.bus.bind("click", list, this.itemSelected, this); |
| var tree = this; |
| this.bus.bind("dblclick", list, function(e) { |
| tree.bus.fire("dblclick", e, tree); |
| }); |
| this.lists.push(list); |
| this.add(list); |
| |
| var splitter = new th.components.Splitter({ attributes: { orientation: th.HORIZONTAL }, scrollbar: new th.components.Scrollbar() }); |
| splitter.scrollbar.style = this.style; |
| splitter.scrollbar.scrollable = list; |
| splitter.scrollbar.opaque = false; |
| this.bus.bind("dragstart", splitter, this.ondragstart, this); |
| this.bus.bind("drag", splitter, this.ondrag, this); |
| this.bus.bind("dragstop", splitter, this.ondragstop, this); |
| |
| this.splitters.push(splitter); |
| this.add(splitter); |
| |
| if (this.parent) this.render(); |
| }, |
| |
| showDetails: function(item) { |
| if (this.details) this.remove(this.details); |
| |
| // var panel = new Panel({ style: { backgroundColor: "white" } }); |
| // var label = new Label({ text: "Some details, please!" }); |
| // panel.add(label); |
| // this.details = panel; |
| // this.add(this.details); |
| |
| if (this.parent) this.repaint(); |
| }, |
| |
| createList: function(items) { |
| var list = new th.components.List({ items: items, style: this.style }); |
| if (this.renderer) list.renderer = this.renderer; |
| list.oldGetRenderer = list.getRenderer; |
| list.getRenderer = function(rctx) { |
| var label = list.oldGetRenderer(rctx); |
| label.attributes.text = rctx.item.name; |
| return label; |
| } |
| return list; |
| }, |
| |
| getSelectedItem: function() { |
| var selected = this.getSelectedPath(); |
| if (selected.length > 0) return selected[selected.length - 1]; |
| }, |
| |
| getSelectedPath: function(asString) { |
| asString = asString || false; |
| var path = []; |
| |
| for (var i = 0; i < this.lists.length; i++) { |
| if (this.lists[i].selected) { |
| path.push(this.lists[i].selected); |
| } else { |
| break; |
| } |
| } |
| |
| if (path.length == 0) return; |
| |
| if (asString) { |
| var result = ''; |
| for (var i = 0; i < path.length - 1; i++) { |
| result += path[i].name + '/'; |
| } |
| if (!path[path.length - 1].contents) { |
| result += path[path.length - 1].name |
| } else { |
| result += path[path.length - 1].name + '/'; |
| } |
| |
| return result; |
| } else { |
| return path; |
| } |
| }, |
| |
| itemSelected: function(e) { |
| var list = e.thComponent; |
| |
| // add check to ensure that list has an item selected; otherwise, bail |
| if (!list.selected) return; |
| |
| var path = []; |
| |
| for (var i = 0; i < this.lists.length; i++) { |
| path.push(this.lists[i].selected); |
| if (this.lists[i] == list) { |
| for (var j = i + 1; j < this.lists.length && this.lists[j].selected; j++) { |
| delete this.lists[j].selected; |
| } |
| break; |
| } |
| } |
| |
| // fire the event AFTER some items maybe got deselected |
| this.bus.fire('itemSelected', {e: e}, this); |
| |
| if (path.length < this.lists.length) { |
| // user selected an item in a previous list; must ditch the subsequent lists |
| var newlists = this.lists.slice(0, path.length); |
| var newsplitters = this.splitters.slice(0, path.length); |
| for (var z = path.length; z < this.lists.length; z++) { |
| this.bus.unbind(this.lists[z]); |
| this.bus.unbind(this.splitters[z]); |
| |
| this.remove(this.lists[z]); |
| this.remove(this.splitters[z]); |
| } |
| this.lists = newlists; |
| this.splitters = newsplitters; |
| } |
| |
| // determine whether to display new list of children or details of selection |
| var newItem = path[path.length-1]; |
| if (newItem && newItem.contents) { |
| this.showChildren(newItem, newItem.contents); |
| } else { |
| this.showDetails(newItem); |
| } |
| }, |
| |
| getItem: function(pathToItem) { |
| var items = this.data; |
| var item; |
| for (var i = 0; i < pathToItem.length; i++) { |
| for (var z = 0; z < items.length; z++) { |
| if (items[z] == pathToItem[i]) { |
| item = items[z]; |
| items = item.contents; |
| break; |
| } |
| } |
| } |
| return item; |
| }, |
| |
| layout: function() { |
| var d = this.d(); |
| |
| var x = d.i.l; |
| for (var i = 0; i < this.lists.length; i++) { |
| var list = this.lists[i]; |
| if (!this.listWidths) this.listWidths = []; |
| if (!this.listWidths[i]) this.listWidths[i] = this.style.defaultSize; |
| var w = this.listWidths[i]; |
| list.bounds = { x: x, y: d.i.t, width: w, height: d.b.h - d.i.h }; |
| |
| x += w; |
| |
| var splitter = this.splitters[i]; |
| w = splitter.getPreferredWidth(-1); |
| splitter.bounds = { x: x, y: d.i.t, width: w, height: d.b.h - d.i.h }; |
| x += w; |
| |
| } |
| |
| if (this.details) { |
| this.details.bounds = { x: x, y: d.i.t, width: 150, height: d.b.h - d.i.h }; |
| } |
| }, |
| |
| paintSelf: function(ctx) { |
| var d = this.d(); |
| |
| if (this.style.backgroundColor) { |
| ctx.fillStyle = this.style.backgroundColor; |
| ctx.fillRect(0, 0, d.b.w, d.b.h); |
| } |
| } |
| }); |
| |
| dojo.declare("th.components.TextArea", th.Container, { |
| /* Some definitions: |
| * Visual line - a phisical line visible on the editor |
| * Text line - The "logical" text is divided into lines of text |
| * which were originally seperated by newlines (\n) |
| * |
| * For example - if a Text-Line is too long, it gets split into |
| * several Visual-Lines |
| * |
| * TODO: make this class compatible with mozilla's coding style |
| */ |
| constructor: function(parms) { |
| this.leftPadding = 10; |
| this.rightPadding = 10 + 16; // the 16 is for the scrollbar. TODO: scrollbar size should not be hardcoded |
| this.style = parms.style || {}; |
| this.lines = []; |
| this.font = this.style.font; |
| // the current position of the cursor |
| this.cursor = { |
| row: 0, // row in screen coordinates |
| col: 0, // column in screen coordinates |
| line: 0, // current text line |
| offset: 0 // offset in text line |
| }; |
| // THE FOLLOWING IS BAD!!! This needs to pass through TH's event bus |
| // I had no alternative here because the keydown event has no (x,y) location |
| // and so according to how the event bus works it can not be assigned to any |
| // specific object. Because of this, the event will not be dispatched. |
| dojo.connect(window, "keypress", this, this.onkeypress); |
| dojo.connect(window, "keydown", this, this.onkeydown); |
| // fvl := First Visible Line |
| this.fvl = 0; |
| |
| // a flag that indicates wheter it is necessary to recalculate the |
| // character dimensions |
| this.recalcCharSize = true; |
| |
| this.scrollbar = new th.components.Scrollbar2(); |
| this.scrollbar.style = this.style; |
| this.scrollbar.scrollable = this; |
| this.scrollbar.opaque = false; |
| this.add(this.scrollbar); |
| this.totalNumberOfLines = 0; |
| }, |
| |
| getScrollInfo: function() { |
| return {offset: this.fvl, span: this.totalNumberOfLines, scope: this.vlc}; |
| }, |
| |
| addText: function(text) { |
| if (typeof text == "string") { |
| var lines = text.split('\n'); // split the text along newlines |
| for (var k = 0; k < lines.length; k++) |
| this.lines.push(lines[k]); |
| } |
| // whenever adding text, we change the span of the text-area |
| // the scrollbar needs to be notified |
| this.scrollbar.layout(); |
| }, |
| |
| paintLine: function(ctx, index, offset) { |
| var line = this.lines[index]; |
| ctx.fillText(line.substr(offset*this.vll, this.vll), 0, 0); |
| }, |
| |
| setFont: function(font) { |
| this.font = font; |
| this.recalcCharSize = true; |
| }, |
| |
| layout: function() { |
| var d = this.d(); |
| this.h = d.b.h; |
| this.w = d.b.w; |
| // set the scrollbar bounds |
| // TODO: the "16" should not be hardcoded |
| this.scrollbar.bounds = { x: this.w - 16, y: 0, height: d.b.h, width: 16}; |
| // Calculate line-height and char-width using the Scene's scratch context |
| if (this.recalcCharSize) { |
| var tmpctx = this.getScratchContext(); |
| tmpctx.font = this.font; |
| this.charSize = tmpctx.measureText("a"); |
| this.recalcCharSize = false; |
| } |
| // I have no idea what ascent means, but in the old editor |
| // implementation, the height is 2.8 times the ascent |
| this.charSize.height = Math.floor(this.charSize.ascent * 2.8); |
| |
| this.scrollbar.increment = 1; |
| |
| // effw := Effective Width (textarea width sans the left&right padding) |
| this.effw = this.w - this.leftPadding - this.rightPadding; |
| // vll := Visual Line Length |
| this.vll = Math.floor(this.effw / this.charSize.width); |
| // vlc := Visible Lines Count |
| this.vlc = Math.floor(this.h / this.charSize.height); |
| // calculate total number of lines (wrapped) |
| this.totalNumberOfLines = 0; |
| for (var k = 0; k < this.lines.length; k++) |
| this.totalNumberOfLines += this.numberOfLines(k); |
| }, |
| |
| scrollUp: function(delta) { |
| if (delta > this.fvl) { |
| delta = this.fvl; |
| this.fvl = 0; |
| } else { |
| this.fvl -= delta; |
| } |
| if (this.cursor.row < this.vlc - delta) |
| this.cursor.row += delta; |
| else { |
| this.cursor.row = this.vlc - 1; |
| var old_offset = this.cursor.offset; |
| var refLine = this.getTextLine(this.fvl + this.vlc - 1); |
| var new_offset = this.lines[refLine.index].length; |
| this.cursor.line = refLine.index; |
| this.cursor.offset = Math.min(old_offset, new_offset); |
| this.cursor.col = this.cursor.offset % this.vll; |
| } |
| }, |
| |
| scrollDown: function(delta) { |
| if (this.fvl + this.vlc + delta > this.totalNumberOfLines) { |
| this.fvl = this.totalNumberOfLines - this.vlc; |
| delta = this.totalNumberOfLines - (this.fvl + this.vlc); |
| } else { |
| this.fvl += delta; |
| } |
| if (this.cursor.row > delta) |
| this.cursor.row -= delta; |
| else { |
| this.cursor.row = 0; |
| var old_offset = this.cursor.offset; |
| var refLine = this.getTextLine(this.fvl); |
| var new_offset = this.lines[refLine.index].length; |
| this.cursor.line = refLine.index; |
| this.cursor.offset = Math.min(old_offset, new_offset); |
| this.cursor.col = this.cursor.offset % this.vll; |
| } |
| }, |
| |
| moveLeft: function() { |
| if (this.cursor.offset > 0) { |
| this.cursor.offset--; |
| if (this.cursor.col > 0) { |
| this.cursor.col--; |
| } else { // I assume (this.cursor.col == 0) |
| this.cursor.col = this.vll - 1; |
| this.cursor.row--; |
| } |
| } else { // I assume (this.cursor.offset == 0) |
| // this also implies that (this.cursor.col == 0) |
| // Handle going up one line |
| // Two options : |
| // 1. I'm in the first line, so do nothing |
| // 2. Some other line, in which case, the cursor should be on |
| // end of the previous line |
| if (this.cursor.line > 0) { |
| this.cursor.line--; |
| // put the cursor at the very end of the line, so that new |
| // text added will be added at the end of the line |
| this.cursor.offset = this.lines[this.cursor.line].length; |
| this.cursor.col = this.cursor.offset % this.vll; |
| this.cursor.row--; |
| } |
| } |
| }, |
| |
| moveRight: function() { |
| if (this.cursor.offset < this.lines[this.cursor.line].length) { |
| this.cursor.offset++; |
| if (this.cursor.col < this.vll - 1) { |
| this.cursor.col++; |
| } else { // I assume (this.cursor.col == this.vll - 1) |
| this.cursor.col = 0; |
| this.cursor.row++; |
| } |
| } else { // I assume (this.cursor.offset == line.length) |
| // I don't care what this.cursor.col is....or should I? |
| // Handle going down one line |
| // Two options : |
| // 1. I'm in the last line, so do nothing |
| // 2. Some other line, in which case, the cursor should be on |
| // beginning of the next line |
| if (this.cursor.line < this.lines.length) { |
| this.cursor.line++; |
| this.cursor.offset = 0; |
| this.cursor.col = 0; |
| this.cursor.row++; |
| } |
| } |
| }, |
| |
| moveDown: function() { |
| if (this.cursor.line < this.lines.length - 1) { |
| this.cursor.line++; |
| |
| var old_offset = this.cursor.offset; |
| var line = this.cursor.line; |
| var new_offset = Math.min(old_offset, this.lines[line].length); |
| var row_offset = Math.floor(old_offset / this.vll) - |
| Math.floor(new_offset / this.vll) + |
| this.numberOfLines(line - 1); |
| this.cursor.row += row_offset; |
| this.cursor.offset = new_offset; |
| this.cursor.col = new_offset % this.vll; |
| } |
| }, |
| |
| moveUp: function() { |
| if (this.cursor.line > 0) { |
| this.cursor.line--; |
| |
| var old_offset = this.cursor.offset; |
| var line = this.cursor.line; |
| var new_offset = Math.min(old_offset, this.lines[line].length); |
| var row_offset = Math.floor(old_offset / this.vll) - |
| Math.floor(new_offset / this.vll) + |
| this.numberOfLines(line); |
| this.cursor.row -= row_offset; |
| this.cursor.offset = new_offset; |
| this.cursor.col = new_offset % this.vll; |
| } |
| }, |
| |
| onkeydown: function(e) { |
| if (e.keyCode >= 37 && e.keyCode <= 40) { |
| // save the first visible line of the editor, so that if it |
| // changes after interaction with the user, the scrollbar needs |
| // to be notified |
| var fvl_before = this.fvl; |
| // left |
| if (e.keyCode == 37) this.moveLeft(); |
| // right |
| if (e.keyCode == 39) this.moveRight(); |
| // up |
| if (e.keyCode == 38) this.moveUp(); |
| // down |
| if (e.keyCode == 40) this.moveDown(); |
| // handle scrolling |
| if (this.cursor.row < 0) { |
| if (-this.cursor.row > this.fvl) |
| this.fvl = 0; |
| else { |
| this.fvl += this.cursor.row; |
| this.cursor.row = 0; |
| } |
| } else if (this.cursor.row >= this.vlc) { |
| if (this.fvl + this.cursor.row + 1 > this.totalNumberOfLines) |
| this.fvl = this.totalNumberOfLines - 1; |
| else { |
| this.fvl += this.cursor.row - this.vlc + 1; |
| this.cursor.row = this.vlc - 1; |
| } |
| } |
| if (this.fvl != fvl_before) |
| this.scrollbar.layout(); |
| this.repaint(); |
| dojo.stopEvent(e); |
| } |
| }, |
| |
| onkeypress: function(e) { |
| if ((e.charCode >= 32) && (e.charCode <= 126) || e.charCode >= 160) { |
| var ch = String.fromCharCode(e.charCode); |
| var textLine = this.cursor.line; |
| var textCol = this.cursor.offset; |
| var line = this.lines[textLine]; |
| if (this.cursor.offset > this.lines[this.cursor.line].length) |
| this.lines[textLine] = line + ch; |
| else |
| this.lines[textLine] = line.substring(0, textCol) + ch + |
| line.substring(textCol, line.length); |
| this.moveRight(); |
| this.repaint(); |
| dojo.stopEvent(e); |
| } |
| }, |
| |
| paintCursor: function(ctx) { |
| ctx.fillStyle = this.style.color; |
| //cvpx, cvpy := cursor visual position x/y |
| var cvpx = this.leftPadding + this.charSize.width * this.cursor.col; |
| var cvpy = this.charSize.height * this.cursor.row; |
| ctx.fillRect(cvpx, cvpy, 1, this.charSize.height); |
| }, |
| |
| numberOfLines: function(k) { |
| // return the number of wrapped lines in text entry 'k' |
| if (k < 0) |
| return Math.ceil(this.lines[0].length / this.vll); |
| if (this.lines[k].length == 0) |
| return 1; |
| return Math.ceil(this.lines[k].length / this.vll); |
| }, |
| |
| getTextLine: function(visual_line) { |
| // For a visual line, find the text line to which it belongs |
| var k = 0; |
| var count = 0; |
| while (count <= visual_line) { |
| if (k == this.lines.length) |
| return {index: k-1, offset: this.numberOfLines(k-1)}; |
| |
| count += this.numberOfLines(k); |
| k++; |
| } |
| count -= this.numberOfLines(k-1); |
| return {index:k-1, offset: visual_line - count}; |
| }, |
| |
| paintSelf: function(ctx) { |
| this.paintCursor(ctx); |
| // draw the text |
| // TODO: I'm not so sure about how to calculate the vertical offset |
| ctx.font = this.font; |
| ctx.fillStyle = this.style.color; |
| var verticalOffset = this.charSize.height * 3 / 4; |
| |
| var firstLine = this.getTextLine(this.fvl); |
| var lastLine = this.getTextLine(this.fvl + this.vlc); |
| var k, startOffset, endOffset; |
| ctx.save(); |
| ctx.translate(this.leftPadding, this.charSize.height * 3 / 4); |
| // If the screen contains just one line, just render from offset to offset |
| if (firstLine.index == lastLine.index) { |
| k = firstLine.index |
| for (var l = firstLine.offset; l < lastLine.offset; l++) { |
| this.paintLine(ctx, k, l); |
| ctx.translate(0, this.charSize.height); |
| } |
| } else { |
| // paint the first line (from offset) |
| k = firstLine.index; |
| startOffset = firstLine.offset; |
| endOffset = this.numberOfLines(k); |
| for (var l = startOffset; l < endOffset; l++) { |
| this.paintLine(ctx, k, l); |
| ctx.translate(0, this.charSize.height); |
| } |
| // paint the lines in between (whole) |
| k++; |
| for (k; k < lastLine.index; k++) { |
| endOffset = this.numberOfLines(k); |
| for (l = 0; l < endOffset; l++) { |
| this.paintLine(ctx, k, l); |
| ctx.translate(0, this.charSize.height); |
| } |
| } |
| // paint the last line (up-to offset) |
| k = lastLine.index; |
| startOffset = 0; |
| endOffset = lastLine.offset; |
| for (l = startOffset; l < endOffset; l++) { |
| this.paintLine(ctx, k, l); |
| ctx.translate(0, this.charSize.height); |
| } |
| } |
| ctx.restore(); |
| } |
| }); |
| |
| /* Scrollbar2 - the next generation |
| * The main changes: |
| * 1. This scrollbar maps from the scrollable's "value" space to the |
| * scrollbar's pixel space, as opposed to "th.components.Scrollbar" |
| * which maps from the scrollable's pixel space to the scrollbar's |
| * pixel space. |
| * 2. This scrollbar does not interfere with the scrollable's properties |
| * but instead, interacts with it using two entry functions: |
| * scrollUp and scrollDown which take as an argument the number of |
| * "value units" the scrollable has to move |
| */ |
| dojo.declare("th.components.Scrollbar2", th.Container, { |
| constructor: function(parms) { |
| if (!parms) parms = {}; |
| this.orientation = parms.orientation || th.VERTICAL; |
| this.value = parms.value || 0; |
| this.min = parms.min || 0; |
| this.max = parms.max || 100; |
| this.increment = parms.increment || 2; |
| |
| this.up = new th.components.Button(); |
| this.down = new th.components.Button(); |
| this.bar = new th.components.Button(); |
| this.add([ this.up, this.down, this.bar ]); |
| |
| this.bus.bind("click", this.up, this.scrollup, this); |
| this.bus.bind("click", this.down, this.scrolldown, this); |
| this.bus.bind("mousedrag", this.bar, this.onmousedrag, this); |
| this.bus.bind("mouseup", this.bar, this.onmouseup, this); |
| }, |
| |
| onmousedrag: function(e) { |
| var currentPosition = (this.orientation == th.VERTICAL) ? e.clientY : e.clientX; |
| |
| if (this.dragstart_mouse === undefined) { |
| this.dragstart_mouse = currentPosition; |
| return; |
| } |
| |
| // difference in pixels; needs to be translated to a difference in value |
| var diff = currentPosition - this.dragstart_mouse; |
| this.dragstart_mouse = currentPosition; |
| |
| // the difference in the value |
| var delta; |
| // Math.floor works differently for negative and positive numbers |
| // (it rounds towards -infinity), so if diff is negative, it will |
| // scroll "slower" than it would if delta is positive. |
| // To correct it, I seperate handling according to the sign of diff. |
| if (diff > 0) |
| delta = Math.floor(diff / this.ratio); |
| else |
| delta = -Math.floor(-diff / this.ratio); |
| |
| this.value += delta; |
| if (this.value < this.min) this.value = this.min; |
| if (this.value > this.max) this.value = this.max; |
| this.layout(); |
| if (this.scrollable) |
| if (delta > 0) |
| this.scrollable.scrollDown(delta); |
| else |
| this.scrollable.scrollUp(-delta); |
| this.repaint(); |
| if (this.scrollable) this.scrollable.repaint(); |
| }, |
| |
| onmouseup: function(e) { |
| delete this.dragstart_value; |
| delete this.dragstart_mouse; |
| }, |
| |
| scrollup: function(e) { |
| if (this.value > this.min) { |
| this.value = Math.min(this.min, this.value - this.increment); |
| if (this.scrollable) this.scrollable.scrollUp(this.increment); |
| this.render(); |
| if (this.scrollable) this.scrollable.repaint(); |
| } |
| }, |
| |
| scrolldown: function(e) { |
| if (this.value < this.max) { |
| this.value = Math.min(this.max, this.value + this.increment); |
| if (this.scrollable) this.scrollable.scrollDown(this.increment); |
| this.render(); |
| if (this.scrollable) this.scrollable.repaint(); |
| } |
| }, |
| |
| layout: function() { |
| var d = this.d(); |
| |
| // check if there's a scrollable attached; if so, refresh state |
| if (this.scrollable !== undefined) { |
| // si := scrollable info |
| var si = this.scrollable.getScrollInfo(); |
| var scrollbar_span = d.b.ih - this.up.bounds.height - this.down.bounds.height |
| this.min = 0; |
| this.max = si.span - si.scope; |
| this.scope = si.scope; |
| this.ratio = scrollbar_span / si.span; |
| this.value = si.offset; |
| } |
| |
| // if the maximum value is less than the minimum, we're in an invalid state and won't paint anything |
| if (this.max < this.min) { |
| for (var i = 0; i < this.children.length; i++) delete this.children[i].bounds; |
| return; |
| } |
| |
| if (this.orientation == th.VERTICAL) { |
| var w = d.b.iw; |
| var h = 12; |
| this.up.bounds = { x: d.i.l + 1, y: d.i.t, width: w, height: h }; |
| this.down.bounds = { x: d.i.l + 1, y: d.b.ih - h, width: w, height: h }; |
| |
| var bar_length = Math.floor(this.ratio * this.scope); |
| var bar_top = Math.floor(this.up.bounds.height + this.ratio * this.value); |
| |
| this.bar.bounds = { x: d.i.l + 1, y: bar_top, width: d.b.iw, height: bar_length } |
| } else { |
| |
| } |
| }, |
| |
| paint: function(ctx) { |
| if (this.max < 0) return; |
| |
| // paint the track |
| if (this.style.scrollTopImage) |
| ctx.drawImage(this.style.scrollTopImage, 1, this.up.bounds.height); |
| if (this.style.scrollMiddleImage) |
| ctx.drawImage(this.style.scrollMiddleImage, 1, |
| this.up.bounds.height + this.style.scrollTopImage.height, |
| this.style.scrollMiddleImage.width, |
| this.down.bounds.y - this.down.bounds.height - (this.up.bounds.x - this.up.bounds.height)); |
| if (this.style.scrollBottomImage) |
| ctx.drawImage(this.style.scrollBottomImage, 1, this.down.bounds.y - this.style.scrollBottomImage.height); |
| |
| // propagate the styles to the children if not already there |
| if (this.style.scrollHandleTopImage && !this.bar.style.topImage) { |
| this.bar.style.topImage = this.style.scrollHandleTopImage; |
| this.bar.style.middleImage = this.style.scrollHandleMiddleImage; |
| this.bar.style.bottomImage = this.style.scrollHandleBottomImage; |
| this.up.style.backgroundImage = this.style.scrollUpArrow; |
| this.down.style.backgroundImage = this.style.scrollDownArrow; |
| } |
| |
| this.inherited(arguments); |
| } |
| }); |
| |
| dojo.declare("th.components.WindowBar", th.Container, { |
| constructor: function(parms) { |
| if (!parms) parms = {}; |
| |
| function loadImg(url) { |
| var img = new Image(); |
| img.src = url; |
| return img; |
| } |
| |
| this.imgBackgroundRight = loadImg('../images/window_top_right.png'); |
| this.imgBackgroundMiddle = loadImg('../images/window_top_middle.png'); |
| this.imgBackgroundLeft = loadImg('../images/window_top_left.png'); |
| |
| this.label = new th.components.Label({ text: parms.title || 'NO TEXT', style: { color: "white", font: "8pt Tahoma" } }); |
| this.label.getInsets = function(){ |
| return { top: 4, left: 6}; |
| } |
| |
| this.imgCloseButton = loadImg('../images/icn_close_x.png'); |
| this.closeButton = new th.components.Button({style: { backgroundImage: this.imgCloseButton}}); |
| |
| this.add(this.label, this.closeButton); |
| |
| this.bus.bind('mousedown', this.closeButton, dojo.hitch(this, function() { |
| this.parentWindow.toggle(); |
| delete this.startValue; |
| })); |
| |
| // make the window dragable :) |
| this.bus.bind("mousedown", this, this.onmousedown, this); |
| this.bus.bind("mouseup", this, this.onmouseup, this); |
| // this event is connected to the window itself, as sometimes the mouse gets outside the WindowBar, event the |
| // mouse is still pressed. This version is working even then right. |
| dojo.connect(window, "mousemove", dojo.hitch(this, this.onmousemove)); |
| }, |
| |
| onmousedown: function(e) { |
| this.startValue = { mouse: { x: e.clientX, y: e.clientY }, window: this.parentWindow.getPosition() }; |
| }, |
| |
| onmousemove: function(e) { |
| if (this.startValue) { |
| var s = this.startValue; |
| var x = s.window.x - (s.mouse.x - e.clientX); |
| var y = s.window.y - (s.mouse.y - e.clientY); |
| this.parentWindow.move(x, y); |
| |
| dojo.stopEvent(e); |
| } |
| }, |
| |
| onmouseup: function(e) { |
| delete this.startValue; |
| }, |
| |
| getPreferredHeight: function() { |
| return 21; |
| }, |
| |
| layout: function() { |
| var d = this.d(); |
| var lh = this.label.getPreferredHeight(d.b.w - 30); |
| this.label.bounds = { y: 0, x: 3, height: lh, width: d.b.w - 20 }; |
| this.closeButton.bounds = { x: d.b.w -14, y: 6 , height: 8, width: 8}; |
| }, |
| |
| paint: function(ctx) { |
| var d = this.d(); |
| |
| ctx.drawImage(this.imgBackgroundLeft, 0, 0); |
| ctx.drawImage(this.imgBackgroundMiddle, 3, 0, d.b.w - 6, 21); |
| ctx.drawImage(this.imgBackgroundRight, d.b.w - 3, 0); |
| |
| this.label.paint(ctx); |
| ctx.drawImage(this.imgCloseButton, d.b.w -14 , 6); |
| } |
| }); |
| |
| dojo.declare("th.components.WindowPanel", th.components.Panel, { |
| constructor: function(title, userPanel) { |
| if (!userPanel) { |
| console.error('The "userPanel" must be given!'); |
| return; |
| } |
| |
| this.userPanel = userPanel; |
| this.windowBar = new th.components.WindowBar({title: title}); |
| this.add([this.windowBar, this.userPanel]); |
| |
| // this is a closed container |
| delete this.add; |
| delete this.remove; |
| }, |
| |
| layout: function() { |
| var d = this.d(); |
| this.width = d.b.w; |
| this.height = d.b.h; |
| var y = this.windowBar.getPreferredHeight(); |
| this.windowBar.bounds = { x: 0, y: 0 , height: y, width: d.b.w }; |
| this.userPanel.bounds = { x: 1, y: y , height: d.b.h - y - 1, width: d.b.w - 2 }; |
| }, |
| |
| paintSelf: function(ctx) { |
| ctx.lineWidth = 2; |
| ctx.strokeStyle = "black"; |
| |
| ctx.strokeStyle = "#2E1F1A"; |
| ctx.strokeRect(0, 0, this.width, this.height); |
| } |
| }); |