1071 lines
33 KiB
JavaScript
1071 lines
33 KiB
JavaScript
/**
|
|
** ==============================
|
|
** 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
|
|
}
|