diff --git a/js/x11/win/windows.js b/js/x11/win/windows.js new file mode 100644 index 0000000..4e3c8d2 --- /dev/null +++ b/js/x11/win/windows.js @@ -0,0 +1,1070 @@ +/** + ** ============================== + ** O O O OOOO + ** O O O O O O + ** O O O O O O + ** OOOO OOOO O OOO OOOO + ** O O O O O O O + ** O O O O O O O + ** OOOO OOOO O O OOOO + ** ============================== + ** Dr. Stefan Bosse http://www.bsslab.de + ** + ** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED + ** BY THE AUTHOR(S). + ** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED, + ** MODIFIED, OR OTHERWISE USED IN A CONTEXT + ** OUTSIDE OF THE SOFTWARE SYSTEM. + ** + ** $AUTHORS: Stefan Bosse + ** $INITIAL: (C) 2006-2021 bLAB + ** $CREATED: 1-10-17 by sbosse. + ** $VERSION: 1.7.24 + ** + ** $INFO: + ** + ** X11/Widget library - can be embedded in any application + ** + ** Create root display and one window: + ** var root = windows({}); + ** + ** root.start(function (err) { + ** console.log('Windows created'); + ** }); + ** var win1 = root.window({width:200,height:300}, function () {console.log('Window 1 exposed')}); + ** + ** Add and modify drawing objects (shapes): + ** + ** win1.add({..}); + ** win1.modify('id',{..}); + ** + ** Add event listener: + ** + ** win1.on('keypress',function (key) {}); + ** + ** Drawing objects: + ** + ** (at least line or fill attribute must be specified) + ** + ** Rectangle: + ** (x,y coordinates: default center point or with align='center') + ** (x,y coordinates: left upper corner with align='left') + ** {id,shape='rect',width,height,x,y,align? + ** line?: {width,color:'black'|..}, + ** fill?: {color:'black'|..}} + ** + ** Triangle: + ** (x,y coordinates: center point) + ** {id,shape='rect',width,height,angle?,x,y, + ** line?: {width,color:'black'|..}, + ** fill?: {color:'black'|..}} + ** + ** Circle/Ellipse: + ** (x,y coordinates: center point) + ** {id,shape='circle',width,height,x,y, + ** line?: {width,color:'black'|..}, + ** fill?: {color: 'black'|..}} + ** + ** Line/Polyline: + ** (x,y: absolute coordinates) + ** {id,shape='line',points:[{x,y},..], + ** line?: {width,style?,color:'black'|..}} + ** + ** Text: + ** (x,y coordinates: default center point or with align='center') + ** (x,y coordinates: left upper corner with align='left') + ** {id,shape='text',x,y,text,style?:{color,align,font,size}} + ** + ** Button: + ** (x,y coordinates: default center point or with align='center') + ** (x,y coordinates: left upper corner with align='left') + ** {id,shape='button',width,height,x,y, + ** label:{text,..},handler:function, + ** line?: {width,color:'black'|..}, + ** fill?: {color:'black'|..}} + ** + ** + ** Pixmap: + ** var Pixmap = new X11.pixmap(); + +* var pixmap = Pixmap.open(pathtofile); // pixmap.data, width, height + ** (x,y coordinates: default center point or with align='center') + ** (x,y coordinates: left upper corner with align='left') + ** {id,shape='pixmap',width,height,x,y, + ** line?: {width,color:'black'|..}, + ** fill?: {color:'black'|..}, + ** data: data} + ** + ** All shape can be interactive via the onclick:function options attribute. + ** The sensitive click region can be extended by the extendClick:number options attribute. + ** + ** Shapes can be modified after drawing: + ** + ** win.modify(shapeid,{label:{text:string,..}}) + ** + ** $ENDOFINFO +*/ + + +var X11 = Require('x11/core/x11'); +var Plot = Require('x11/win/plot'); +var Comp = Require('com/compat'); +var KeyPress = X11.eventMask.KeyPress; +var ButtonPress = X11.eventMask.ButtonPress; +var Exposure = X11.eventMask.Exposure; +var PointerMotion = X11.eventMask.PointerMotion; +var Rtree = Require('x11/win/rtree'); + +function rotate(cx, cy, x, y, angle) { + var radians = (Math.PI / 180) * angle, + cos = Math.cos(radians), + sin = Math.sin(radians), + nx = (cos * (x - cx)) + (sin * (y - cy)) + cx, + ny = (cos * (y - cy)) - (sin * (x - cx)) + cy; + return [nx, ny]; +} +function flatten (points) { + return points.reduce(function (acc, val) { return acc.concat(val)},[]);; +} +// Can be extended! +var color_palette = { + beig:[245,245,220], + black:[0,0,0], + blue:[0,0,255], + brown:[165,42,42], + coral:[255,127,80], + crimson:[220,20,60], + cyan:[0,255,255], + gold:[255,215,0], + gray:[128,128,128], + gray5:[242,242,242], + gray10:[230,230,230], + gray15:[216,216,216], + gray20:[204,204,204], + gray25:[191,191,191], + gray30:[178,178,178], + gray35:[165,165,165], + gray40:[152,152,152], + gray45:[140,140,140], + gray50:[128,128,128], + gray55:[114,114,114], + gray60:[102,102,102], + gray65:[89,89,89], + gray70:[76,76,76], + gray75:[63,63,63], + gray80:[51,51,51], + gray50:[38,38,38], + gray90:[25,25,25], + gray95:[12,12,12], + green:[0,200,0], + indigo:[75,0,130], + lime:[0,255,0], + magenta:[255,0,255], + maroon:[128,0,0], + navy:[0,0,128], + olive:[128,128,0], + orange:[255,179,0], + peru:[205,133,63], + pink:[255,192,203], + purple:[128,0,128], + red:[255,0,0], + salmon:[250,128,114], + sienna:[160,82,45], + silver:[192,192,192], + tan:[210,180,140], + turquoise:[64,224,208], + violet:[238,130,238], + white:[255,255,255], + yellow:[255,242,0], +} +function update(dst,src) { + for(var a in src) { + if (typeof dst[a] == 'object' && typeof src[a] == 'object') + update(dst[a],src[a]); + else + dst[a]=src[a]; + } +} + +function modifies(attr,shape) { + for(var a in attr) { + if (typeof attr[a] == 'object' && typeof shape[a] == 'object') + return modifies(attr[a],shape[a]); + else if (attr[a] != shape[a]) return true; + } + return false; +} + +/******************* +** RTREE Object +*******************/ + +// rtree revision 2 using HP rtree +function rtree(w,h) { + if (!(this instanceof rtree)) return new rtree(w,h); + this.bbox={x0:0,y0:0,x1:w,y1:h}; + this.root=Rtree(); + this.within=this.root.within; + this.bboxGroup=this.root.BBoxGroup; + this.equal=this.root.equal; +} +// add shape +rtree.prototype.add = function (shape) { + var bbox=this.bboxOf(shape); + shape.bbox=bbox; + this.root.insert({x0:bbox.x0,y0:bbox.y0,x1:bbox.x1,y1:bbox.y1,shape:shape}); +} +// Compute bbox of a shape +rtree.prototype.bboxOf = function (shape) { + var ox0=0,oy0=0,x0=0,y0=0,x1=0,y1=0,i,p,first=true; + switch (shape.shape) { + case 'rect': + case 'circle': + case 'triangle': + case 'button': + case 'pixmap': + if (!shape.align || shape.align=='center') ox0=shape.width/2,oy0=shape.height/2; + x0=shape.x-ox0; + y0=shape.y-oy0; + x1=x0+shape.width; + y1=y0+shape.height; + break; + case 'line': + for (i in shape.points) { + p=shape.points[i]; + if (first) x0=p.x,y0=p.y,x1=p.x,y1=p.y,first=false; + x0=Math.min(x0,p.x),y0=Math.min(y0,p.y), + x1=Math.max(x1,p.x),y1=Math.max(y1,p.y); + } + break; + case 'text': + // TODO: Rough approx. + switch (shape.style && shape.style.align) { + case 'center': + x0=shape.x-shape.width/2; + y0=shape.y-shape.height/2; + x1=x0+shape.width/2; + y1=shape.y+shape.height/2; + break; + default: + x0=shape.x; + y0=shape.y-shape.height; + x1=x0+shape.width; + y1=shape.y; + } + } + return {x0:x0,y0:y0,x1:x1,y1:y1}; +} +// remove shape +rtree.prototype.delete = function (shape) { + var node = this.root.search(shape.bbox).find(function (n) {return n.shape.id==shape.id}); + if (node) this.root.remove(node); +} +// Find shape node in tree +rtree.prototype.find = function (shape) { + return this.root.search(shape.bbox).find(function (n) {return n.shape.id==shape.id}); +} +// Find all shapes overlapping with bounding box +rtree.prototype.findAll = function (bbox) { + return this.root.search(bbox); +} +rtree.prototype.print = function (node,indent) { + return this.root.print(); +} +rtree.prototype.printBbox = function (bbox) { + if (bbox.bbox) bbox=bbox.bbox; + return bbox.x0+','+bbox.y0+'-'+bbox.x1+','+bbox.y1; +} + + +/******************* +** WINDOW Object +*******************/ + +function window(options) { + var self=this; + if (!(this instanceof window)) return new window(options); + this.wid=0; + // basic color space + this.gc={}; + this.ready=false; + this.suspended=false; + this.lazy=options.lazy||0; // drawing update with timers (-1:never) + this.pending=[]; // pending scheduling block + this.events={}; // event handlers + this.objects={}; // all primitive drawing objects + this.plots={}; // all plot objects + this.buttons={}; // any region handling mouse clicks + this.rtree=rtree(options.width, options.height); // draw object management + this.z = {min:Number.MAX_VALUE,max:Number.MIN_VALUE}; + this.visible=false; + this.background = options.background||'white'; + this.fonts = { + fixed : {size:options.fontSize||10,id:0} + }; + this.on('click',function (pos) { + var bbox={x0:pos.x-1,y0:pos.y-1,x1:pos.x+1,y1:pos.y+1}; + for(var bid in self.buttons) { + if (!self.buttons[bid]) continue; + if (self.rtree.within(bbox,self.buttons[bid].bbox) && !self.buttons[bid].shape.hidden) { + self.buttons[bid].handler(pos); + return; + } + } + }); + if (this.lazy>0) this.updater=setInterval(function () { self.update() },this.lazy); +} + +// Add a shape +window.prototype.add = function (shape) { + var fs; + if (shape.id == undefined) shape.id='Shape'+Object.keys(this.objects).length; + shape.redraw=true; + if (shape.z==undefined) shape.z=0; + switch (shape.shape) { + case 'line': + shape._points=[].concat.apply([],shape.points.map(function (p) {return [p.x,p.y]})); + break; + case 'text': + // TODO: use text extent + fs=(shape.style && shape.style.size)||this.fonts.fixed.size; + shape.height=shape.height||fs; + shape.width=shape.width||int(shape.text.length*fs*0.7); + shape.width0=shape.width; + shape.x0 = shape.x; + shape.y0 = shape.y; + switch (shape.style && shape.style.align) { + case 'center': + shape.height=shape.height||fs; + shape.width=shape.width||int(shape.text.length*fs*0.7); + // shape.width0=shape.width; + shape.x0 = shape.x; + shape.y0 = shape.y; + shape.x = shape.x0 - int(shape.width/2); + shape.y = shape.y0 + int(shape.height/2); + break; + } + break; + case 'button': + // TODO: use text extent + fs=shape.label.size||this.fonts.fixed.size; + shape.label.height=shape.label.height||fs; + shape.label.width=shape.label.width||int(shape.label.text.length*fs*0.7); + shape.label.x = shape.x - int(shape.label.width/2); + shape.label.y = shape.y + int(shape.label.height/2); + this.buttons[shape.id]={handler:shape.handler,bbox:this.rtree.bboxOf(shape),shape:shape}; + break; + } + if (shape.shape != 'button' && shape.onclick) { + var bbox=this.rtree.bboxOf(shape); + if (shape.extendClick) { + // enlarg sensisive regione + bbox.x0 -= shape.extendClick; + bbox.y0 -= shape.extendClick; + bbox.x1 += shape.extendClick; + bbox.y1 += shape.extendClick; + } + this.buttons[shape.id]={handler:shape.onclick,bbox:bbox,shape:shape}; + } + this.rtree.add(shape); + + // Pending clear operation of previous shape with same id? + if (this.objects[shape.id] && this.objects[shape.id].clear && !this.equal(this.objects[shape.id],shape,true)) { + this.clear(this.objects[shape.id]); + } + this.objects[shape.id]=shape; + //console.log(this.rtree.print()); + this.z.min=Math.min(shape.z,this.z.min); + this.z.max=Math.max(shape.z,this.z.max); + // TODO: redraw overlapping objects, too + //if (!this.lazy) + this.emit('Redraw'); + return shape.id; +} + +// Clear one shape/erase to background +window.prototype.clear = function (shapeOrId, background) { + var shape=(typeof shapeOrId == 'object'?shapeOrId:this.objects[shapeOrId]), + gc, + x0=0,y0=0, + X = this.X, + self=this, + todo=[]; + + if (!background) background=shape.backgound||this.background||'white'; + switch (shape.shape) { + case 'rect': + case 'button': + if (!shape.align || shape.align=='center') + x0=shape.width/2,y0=shape.height/2; // Default: (x,y) is center point + if (shape.fill) { + gc=self.gc[background]; + X.PolyFillRectangle(self.wid, gc, [shape.x-x0, + shape.y-y0, + shape.width, shape.height]); + } + if (shape.line) { + gc=self.gc[background]; + X.ChangeGC(gc, { lineWidth:shape.line.width||1 }); + X.PolyRectangle(self.wid, gc, [shape.x-x0, + shape.y-y0, + shape.width, shape.height]); + X.ChangeGC(gc, { lineWidth:1 }); + } + if (shape.label) { + gc=self.gc[background]; + X.PolyText8(self.wid, gc, shape.label.x, shape.label.y, [shape.label.text]) + } + break; + case 'triangle': + var points=[ + shape.x-shape.width/2, shape.y+shape.height/2, + shape.x+shape.width/2, shape.y+shape.height/2, + shape.x, shape.y-shape.height/2, + shape.x-shape.width/2, shape.y+shape.height/2 + ] + if (shape.angle) { + points=points.map(function (p) { + return rotate(shape.x,shape.y,p[0],p[1],shape.angle); + }); + } + points=flatten(points); + if (shape.fill) { + gc=self.gc[background]; + X.FillPoly(self.wid, gc, 0, 0, points); + } + if (shape.line) { + gc=self.gc[background]; + X.ChangeGC(gc, { lineWidth:shape.line.width||1 }); + X.PolyLine(0, self.wid, gc, points); + X.ChangeGC(gc, { lineWidth:1 }); + } + break; + case 'circle': + if (shape.fill) { + gc=self.gc[background]; + X.PolyFillArc(self.wid, gc, [shape.x-shape.width/2, + shape.y-shape.height/2, + shape.width, shape.height, 0, 360*64]); + } + if (shape.line) { + gc=self.gc[background]; + X.ChangeGC(gc, { lineWidth:shape.line.width||1 }); + X.PolyArc(self.wid, gc, [shape.x-shape.width/2, + shape.y-shape.height/2, + shape.width, shape.height, 0, 360*64]); + X.ChangeGC(gc, { lineWidth:1 }); + } + break; + case 'line': + if (shape.line) { + gc=self.gc[background]; + X.ChangeGC(gc, { lineWidth:shape.line.width||1 }); + X.PolyLine(0, self.wid, gc, shape._points); + X.ChangeGC(gc, { lineWidth:1 }); + } else { + gc=self.gc['background']; + X.PolyLine(0, self.wid, gc, shape._points); + } + break; + case 'text': + gc=self.gc[background]; + X.PolyText8(self.wid, gc, shape.x, shape.y, [shape.text]) + break; + case 'pixmap': + if (!shape.align || shape.align=='center') + x0=shape.width/2,y0=shape.height/2; // Default: (x,y) is center point + gc=self.gc[background]; + X.PolyFillRectangle(self.wid, gc, [shape.x-x0, + shape.y-y0, + shape.width, shape.height]); + break; + } +} + +// Hider or show shapes (display=none|visible) +window.prototype.display = function (shapeOrId, attr) { + var shape=(typeof shapeOrId == 'object'?shapeOrId:this.objects[shapeOrId]); + if (attr=='none' || attr=='hidden') { + if (!shape.hidden) { + shape.hidden=true; + this.clear(shapeOrId); + } + } else { + shape.hidden=false; + this.draw(shapeOrId); + } +} + +// hide (unmap) window +window.prototype.hide = function () { + this.X.UnmapWindow(this.wid); + this.visible=false; +} + +// Draw one shape +window.prototype.draw = function (shapeOrId) { + var shape=(typeof shapeOrId == 'object'?shapeOrId:this.objects[shapeOrId]); + var gc, + x0=0,y0=0, + X = this.X, + self=this, + todo=[]; + var background=shape.backgound||'white'; + if (shape.hidden) return; + switch (shape.shape) { + case 'rect': + case 'button': + if (!shape.align || shape.align=='center') + x0=shape.width/2,y0=shape.height/2; // Default: (x,y) is center point + if (shape.fill) { + if (shape.fill.color && self.gc[shape.fill.color]) + gc=self.gc[shape.fill.color]; + else + gc=self.gc.black; + X.PolyFillRectangle(self.wid, gc, [shape.x-x0, + shape.y-y0, + shape.width, shape.height]); + } + if (shape.line) { + if (shape.line.color && self.gc[shape.line.color]) + gc=self.gc[shape.line.color]; + else + gc=self.gc.black; + X.ChangeGC(gc, { lineWidth:shape.line.width||1 }); + X.PolyRectangle(self.wid, gc, [shape.x-x0, + shape.y-y0, + shape.width, shape.height]); + X.ChangeGC(gc, { lineWidth:1 }); + } + if (shape.label) { + if (shape.label.color) + gc=self.gc[shape.label.color]; + else + gc=self.gc.black; + X.PolyText8(self.wid, gc, shape.label.x, shape.label.y, [shape.label.text]) + } + break; + case 'triangle': + var points=[ + [shape.x-shape.width/2, shape.y+shape.height/2], + [shape.x+shape.width/2, shape.y+shape.height/2], + [shape.x, shape.y-shape.height/2], + [shape.x-shape.width/2, shape.y+shape.height/2] + ] + if (shape.angle) { + points=points.map(function (p) { + return rotate(shape.x,shape.y,p[0],p[1],shape.angle); + }); + } + points = flatten(points); + if (shape.fill) { + if (shape.fill.color && self.gc[shape.fill.color]) + gc=self.gc[shape.fill.color]; + else + gc=self.gc.black; + X.FillPoly(self.wid, gc, 0, 0, points); + } + if (shape.line) { + if (shape.line.color && self.gc[shape.line.color]) + gc=self.gc[shape.line.color]; + else + gc=self.gc.black; + X.ChangeGC(gc, { lineWidth:shape.line.width||1 }); + X.PolyLine(0, self.wid, gc, points); + X.ChangeGC(gc, { lineWidth:1 }); + } + break; + case 'circle': + if (shape.fill) { + if (shape.fill.color && self.gc[shape.fill.color]) + gc=self.gc[shape.fill.color]; + else + gc=self.gc.black; + X.PolyFillArc(self.wid, gc, [shape.x-shape.width/2, + shape.y-shape.height/2, + shape.width, shape.height, 0, 360*64]); + } + if (shape.line) { + if (shape.line.color && self.gc[shape.line.color]) + gc=self.gc[shape.line.color]; + else + gc=self.gc.black; + X.ChangeGC(gc, { lineWidth:shape.line.width||1 }); + X.PolyArc(self.wid, gc, [shape.x-shape.width/2, + shape.y-shape.height/2, + shape.width, shape.height, 0, 360*64]); + X.ChangeGC(gc, { lineWidth:1 }); + } + break; + case 'line': + if (shape.line) { + if (shape.line.color && self.gc[shape.line.color]) + gc=self.gc[shape.line.color]; + else + gc=self.gc.black; + X.ChangeGC(gc, { lineWidth:shape.line.width||1 }); + X.PolyLine(0,self.wid, gc, shape._points); + X.ChangeGC(gc, { lineWidth:1 }); + } else { + gc=self.gc.black; + X.PolyLine(0,self.wid, gc, shape._points); + } + break; + case 'text': + if (shape.style && shape.style.color) + gc=self.gc[shape.style.color]; + else + gc=self.gc.black; + X.PolyText8(self.wid, gc, shape.x, shape.y, [shape.text]) + break; + case 'pixmap': + if (!shape.align || shape.align=='center') + x0=shape.width/2,y0=shape.height/2; // Default: (x,y) is center point + gc = self.gc[background]; + X.PutImage(2, self.wid, gc, shape.width, shape.height, shape.x-x0, shape.y-y0, 0, shape.depth||24, shape.data); + break; + } + shape.redraw=false; +} + + +window.prototype.emit = function (ev,arg) { + // console.log('EMIT ['+this.wid+'] '+ev); + if (this.events[ev]) this.events[ev](arg); +} + +// Are two shapes equal? +window.prototype.equal = function (shape1,shape2,geo) { + if (shape1.shape!=shape2.shape) return false; + if (!this.rtree.equal(shape1.bbox,shape2.bbox)) return false; + if (geo) return true; + return false; +} +// Erase window +window.prototype.erase = function (background) { + +} + +// return visual or plot object +window.prototype.get = function (id) { + if (this.objects[id]) return this.objects[id]; + if (this.plots[id]) return this.plots[id]; +} + +// Modify a shape or plot object (or a part of a shape/node) +window.prototype.modify = function (shapeOrId,attr,part) { + var shape=(typeof shapeOrId == 'object'?shapeOrId:this.objects[shapeOrId]), + plot=(typeof shapeOrId == 'string'?this.plots[shapeOrId]:none), + objs, + background, + move= attr && (attr.x!=undefined || attr.y!=undefined), + repaint= attr && (attr.width!=undefined || attr.height!=undefined || attr.text || attr.label); + + if (plot) Plot.modify.call(this,shapeOrId,attr,part); + if (!shape) return; + + if (!modifies(attr,shape)) return; // no change; no redraw + + // Get all overlapping shapes with this shape + objs=this.overlap(shape); + + // Avoid redrawing of overlapping shapes if this shape is within the shape behind + if (objs.length>1 && objs[0].id==shape.id && this.rtree.within(shape.bbox,objs[1].bbox)) + // Clear and redraw only this shape + background=this.style(objs[1],'background'); + else + // Redraw all underlying and overlying objects, too! + objs.map(function (s) {s.redraw=true}); + + shape.redraw=true; + // This shape must be cleared if moved/resized! + if (move || repaint) this.clear(shape,shape.background||background); + + //console.log('>>',shape.id+'['+self.rtree.printBbox(shape)+']',objs.map(function (s) {return s.id+'['+self.rtree.printBbox(s)+']'}),'<<'); + update(shape,attr); + + if (attr.text) { + // TODO: use text extent + fs=(shape.style && shape.style.size)||this.fonts.fixed.size; + switch (shape.style && shape.style.align) { + case 'center': + shape.width=shape.width0||int(shape.text.length*fs*0.7); + shape.x = shape.x0 - int(shape.width/2); + shape.y = shape.y0 + int(shape.height/2); + break; + } + } + if (attr.label) { + // TODO: use text extent + fs=(shape.label.size)||this.fonts.fixed.size; + shape.label.width=int(shape.label.text.length*fs*0.7); + shape.label.x = shape.x - int(shape.label.width/2); + shape.label.y = shape.y + int(shape.label.height/2); + } + if (!this.lazy && !this.suspended) { + for(i in objs) + if (objs[i].redraw) this.draw(objs[i]); + } else { + // we redraw this and the overlapping shapes later + shape.background=background; + } + + // update mouse area, too + if (this.buttons[shape.id]) { + var bbox=this.rtree.bboxOf(shape); + if (shape.extendClick) { + // enlarg sensisive regione + bbox.x0 -= shape.extendClick; + bbox.y0 -= shape.extendClick; + bbox.x1 += shape.extendClick; + bbox.y1 += shape.extendClick; + } + this.buttons[shape.id]={handler: this.buttons[shape.id].handler,bbox:bbox,shape:shape}; } +} + +// Move a shape +window.prototype.move = function (shapeOrId,dx,dy) { + var shape=(typeof shapeOrId == 'object'?shapeOrId:this.objects[shapeOrId]), + attr={}; + if (dx) attr.x=shape.x+dx; + if (dy) attr.y=shape.y+dy; + + this.modify(shape,attr) +} +window.prototype.moveTo = function (shapeOrId,x,y) { + var shape=(typeof shapeOrId == 'object'?shapeOrId:this.objects[shapeOrId]), + attr={}; + if (x) attr.x=x; + if (y) attr.y=y; + + this.modify(shape,attr) +} + +// Remove event handler +window.prototype.off = function (ev) { + this.events[ev]=undefined; +} + +// Add event handler +window.prototype.on = function (ev,handler) { + this.events[ev]=handler; +} + +// Find all overlapping shapes and return list in decreasing z order +window.prototype.overlap = function (shape) { + var objs, + self=this, + bbox=shape.bbox; + if (!shape.fill) switch (shape.shape) { + case 'rect': + // Optimization: Only find objects overlapping with rectangle lines! + objs=this.rtree.findAll({x0:bbox.x0-1,y0:bbox.y0-1,x1:bbox.x0+1,y1:bbox.y1+1}); + objs=objs.concat(this.rtree.findAll({x0:bbox.x1-1,y0:bbox.y0-1,x1:bbox.x1+1,y1:bbox.y1+1})); + objs=objs.concat(this.rtree.findAll({x0:bbox.x0-1,y0:bbox.y0-1,x1:bbox.x1+1,y1:bbox.y0+1})); + objs=objs.concat(this.rtree.findAll({x0:bbox.x0-1,y0:bbox.y1-1,x1:bbox.x1+1,y1:bbox.y1+1})); + break; + } + // Phase 1: get all shapes overlapping with this shape + if (!objs) objs=this.rtree.findAll(bbox); + // Phase 2: Get bbox of all shapes + bbox=this.rtree.bboxGroup(objs); + // Phase 3: find all shapes from this list overlapping with any other shape + objs=this.rtree.findAll(bbox).map(function (n) { return n.shape}); + + return objs.sort(function(a,b) { return a.z>',shape.id+'['+self.rtree.printBbox(shape)+']',objs.map(function (s) {return s.id+'['+self.rtree.printBbox(s)+']'}),'<<'); + + if (!this.lazy && !this.suspended) { + delete this.objects[shape.id]; + this.clear(shape,background); + for(i in objs) + if (objs[i].redraw) this.draw(objs[i]); + } else { + // we can clear shape and redraw overlapping shapes later + shape.clear=true; + shape.background=background; + } +} + +// Resume window updates (enable redrawing) +window.prototype.resume = function () { + this.suspended=false; + this.update(); +} + + +// Suspend window updates (disable redrawing) +window.prototype.suspend = function () { + this.suspended=true; +} + +// show (map) window +window.prototype.show = function () { + this.X.MapWindow(this.wid); + this.visible=true; +} + +// Get shape style attribute +// {'background','foreground'} +window.prototype.style = function (shape,attr) { + switch (attr) { + case 'background': + if (shape.fill && shape.fill.color) return shape.fill.color; + } + return 'white'; +} + +// Redraw all objects with redraw flag set with respect to their z layer +// Update object operations (redraw,clear,..) +window.prototype.update = function (all) { + var updated=0; + if (!this.ready) return 0; + for(var z=this.z.min;z<=this.z.max;z++) { + for(var i in this.objects) { + var shape=this.objects[i]; + if (!shape || (!shape.redraw && !shape.clear && !all) || shape.z!=z) continue; + if (shape.clear) {this.clear(shape);delete this.objects[shape.id]; updated++} + else {this.draw(shape);updated++}; + } + } + return updated; +} + +/******************************** +** WINDOWS FRAMEWORK Object +********************************/ + +function windows(options) { + if (!(this instanceof windows)) return new windows(options); + this.options=options||{}; + this.init=false; + this.ready=false; + this.windows=[]; + this.pending=[]; + this.events=[]; + this.palette=color_palette; +} + +windows.prototype.addColor = function (name,r,g,b) { + this.palette[name]=[r,g,b]; +} + +windows.prototype.emit = function (ev,arg,id) { + // console.log('EMIT '+ev+' ['+id+']'); + if (id!=undefined && this.windows[id]) this.windows[id].emit(ev,arg); + else if (this.events[ev]) this.events[ev](arg); +} + +// Event handler +windows.prototype.handler = function (ev) { + //console.log(ev) + switch (ev.type) { + case X11.eventNumber.Expose: + this.emit('Expose',undefined,ev.wid); + break; + case X11.eventNumber.KeyPress: + var key = this.kk2Name[ev.keycode][0]; + this.emit('keypress',key,ev.wid); + break; + case X11.eventNumber.ButtonPress: + this.emit('click',{x:ev.x,y:ev.y},ev.wid); + break; + } +} + +windows.prototype.off = function (ev) { + this.events[ev]=undefined; +} + +windows.prototype.on = function (ev,handler) { + this.events[ev]=handler; +} + +windows.prototype.start = function (callback) { + var self=this; + if (this.init) return; + this.init=true; + X11.createClient(function(err, display) { + if (!err) { + var X = self.X = display.client; + self.display = display; + self.root = display.screen[0].root; + self.white = display.screen[0].white_pixel; + self.black = display.screen[0].black_pixel; + self.kk2Name = {}; + + // Start event listener + X.on('event', function(ev) { + self.handler(ev); + }); + X.on('error', function(e) { + console.log(e); + }); + + var todo = [ + function (next) { + var ks = X11.keySyms; + var ks2Name = {}; + for (var key in ks) ks2Name[ ks[key].code ] = key; + var min = display.min_keycode; + var max = display.max_keycode; + X.GetKeyboardMapping(min, max-min, function(err, list) { + for (var i=0; i < list.length; ++i) + { + var name = self.kk2Name[i+min] = []; + var sublist = list[i]; + for (var j =0; j < sublist.length; ++j) + name.push(ks2Name[sublist[j]]); + }; + next(); + }); + }, + function (next) { + self.ready=true; + if (callback) callback(null,display); + if (self.pending) { + X11.block(self.pending); + self.pending=[]; + } + } + ]; + X11.block(todo); + } else { + if (callback) callback(err); + } + }); + +} + +// Create and add a new window +windows.prototype.window = function (options,callback) { + var win = window(options), + self=this, + X = this.X; + + function expose() { + win.update(true); + } + function createColors(next) { + var n=0,len=Object.keys(color_palette).length; + function createColor(name,r,g,b) { + X.AllocColor(self.display.screen[0].default_colormap, r*255, g*255, b*255, function (err,color) { + if (err) console.log(err); + X.CreateGC(win.gc[name], win.wid, + { + foreground: color.pixel, + background: self.white, + font: win.fonts.fixed.id + }); + n++; + if (n==len) next(); + }); + } + for(var c in color_palette) { + win.gc[c]=X.AllocID(); + switch (c) { + case 'black': + X.CreateGC(win.gc.black, win.wid, { foreground: self.black, background: self.white, font: win.fonts.fixed.id}); + n++; + break; + case 'white': + X.CreateGC(win.gc.white, win.wid, { foreground: self.white, background: self.white, font: win.fonts.fixed.id}); + n++; + break; + default: + createColor(c,color_palette[c][0],color_palette[c][1],color_palette[c][2]); + } + } + } + var todo = [ + function (next) { + X = self.X; + win.gc = {}; + win.wid = X.AllocID(); + self.windows[win.wid]=win; + console.log('Creating new window '+win.wid); + win.X=X; + X.CreateWindow( + win.wid, self.root, // new window id, parent + options.x||0, options.y||0, + options.width, options.height, // x, y, w, h + 0, 0, 0, 0, // border, depth, class, visual + { + backgroundPixel: options.background=='black'?self.black:self.white, + eventMask: KeyPress|ButtonPress|Exposure + } // other parameters + ); + X.MapWindow(win.wid); + win.visible=true; + X.ChangeProperty(0, win.wid, X.atoms.WM_NAME, X.atoms.STRING, 8, options.title||''); + win.on('Expose',next); + }, + function (next) { + if (!win.fonts.fixed.id) { + win.fonts.fixed.id=X.AllocID(); + win.fonts.fixed.size=options.fontSize||14; + X.OpenFont('-*-'+(options.fontFamily?options.fontFamily:'fixed')+ + '-*-r-*-*-'+win.fonts.fixed.size+ + '-*-*-*-*-*-*-*',win.fonts.fixed.id); + if (options.verbose) console.log('Default Font: -*-'+(options.fontFamily?options.fontFamily:'fixed')+ + '-*-r-*-*-'+win.fonts.fixed.size+ + '-*-*-*-*-*-*-*',win.fonts.fixed.id); + } + next(); + }, + function (next) { + win.off('Expose'); + createColors(next); + }, + function (next) { + // console.log('WIN REDAY!'); + win.ready=true; + win.on('Expose',expose); + win.on('Redraw',win.update.bind(win)); + win.emit('Redraw'); + if (callback) callback(); + next(); + } + ]; + if (!this.ready) this.pending=this.pending.concat(todo); else X11.block(todo); + return win; +}; + +module.exports = { + windows:windows +}