jam/js/x11/win/windows.js

1071 lines
33 KiB
JavaScript
Raw Normal View History

2025-07-21 23:03:50 +02:00
/**
** ==============================
** 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<b.z});
}
/** Set window title
*
*/
window.prototype.title = function (title) {
this.X.ChangeProperty(0, this.wid, this.X.atoms.WM_NAME, this.X.atoms.STRING, 8, title);
}
window.prototype.plot = Plot.window.prototype.plot;
// Remove a shape
window.prototype.remove = function (shape) {
var objs,background,i,
self=this;
if (typeof shape == 'string') shape=this.get(shape);
if (!shape) return;
// remove shape
this.rtree.delete(shape);
// Get all overlapping shapes w/o this shape
objs=this.overlap(shape);
// Avoid redrawing of overlapping shapes if this shape is within the shape behind
if (objs.length && this.rtree.within(shape.bbox,objs[0].bbox))
// Clear and redraw only this shape
background=this.style(objs[0],'background');
else
// Redraw all underlying and overlying objects, too!
objs.map(function (s) {s.redraw=true});
//console.log('>>',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
}