fd75c7c7b4
When the bubble is near the screen border, draw a half arrow at the corner of the bubble. https://bugzilla.gnome.org/show_bug.cgi?id=639979
483 lines
19 KiB
JavaScript
483 lines
19 KiB
JavaScript
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const Lang = imports.lang;
|
|
const St = imports.gi.St;
|
|
const Shell = imports.gi.Shell;
|
|
|
|
const Tweener = imports.ui.tweener;
|
|
|
|
const POPUP_ANIMATION_TIME = 0.15;
|
|
|
|
/**
|
|
* BoxPointer:
|
|
* @side: side to draw the arrow on
|
|
* @binProperties: Properties to set on contained bin
|
|
*
|
|
* An actor which displays a triangle "arrow" pointing to a given
|
|
* side. The .bin property is a container in which content can be
|
|
* placed. The arrow position may be controlled via setArrowOrigin().
|
|
*
|
|
*/
|
|
function BoxPointer(side, binProperties) {
|
|
this._init(side, binProperties);
|
|
}
|
|
|
|
BoxPointer.prototype = {
|
|
_init: function(arrowSide, binProperties) {
|
|
this._arrowSide = arrowSide;
|
|
this._arrowOrigin = 0;
|
|
this._arrowCorner = null;
|
|
this.actor = new St.Bin({ x_fill: true,
|
|
y_fill: true });
|
|
this._container = new Shell.GenericContainer();
|
|
this.actor.set_child(this._container);
|
|
this._container.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
|
|
this._container.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
|
|
this._container.connect('allocate', Lang.bind(this, this._allocate));
|
|
this.bin = new St.Bin(binProperties);
|
|
this._container.add_actor(this.bin);
|
|
this._border = new St.DrawingArea();
|
|
this._border.connect('repaint', Lang.bind(this, this._drawBorder));
|
|
this._container.add_actor(this._border);
|
|
this.bin.raise(this._border);
|
|
},
|
|
|
|
show: function(animate, onComplete) {
|
|
let x = this.actor.x;
|
|
let y = this.actor.y;
|
|
let themeNode = this.actor.get_theme_node();
|
|
let rise = themeNode.get_length('-arrow-rise');
|
|
|
|
this.actor.opacity = 0;
|
|
this.actor.show();
|
|
|
|
if (animate) {
|
|
switch (this._arrowSide) {
|
|
case St.Side.TOP:
|
|
this.actor.y -= rise;
|
|
break;
|
|
case St.Side.BOTTOM:
|
|
this.actor.y += rise;
|
|
break;
|
|
case St.Side.LEFT:
|
|
this.actor.x -= rise;
|
|
break;
|
|
case St.Side.RIGHT:
|
|
this.actor.x += rise;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Tweener.addTween(this.actor, { opacity: 255,
|
|
x: x,
|
|
y: y,
|
|
transition: "linear",
|
|
onComplete: onComplete,
|
|
time: POPUP_ANIMATION_TIME });
|
|
},
|
|
|
|
hide: function(animate, onComplete) {
|
|
let x = this.actor.x;
|
|
let y = this.actor.y;
|
|
let originalX = this.actor.x;
|
|
let originalY = this.actor.y;
|
|
let themeNode = this.actor.get_theme_node();
|
|
let rise = themeNode.get_length('-arrow-rise');
|
|
|
|
if (animate) {
|
|
switch (this._arrowSide) {
|
|
case St.Side.TOP:
|
|
y += rise;
|
|
break;
|
|
case St.Side.BOTTOM:
|
|
y -= rise;
|
|
break;
|
|
case St.Side.LEFT:
|
|
x += rise;
|
|
break;
|
|
case St.Side.RIGHT:
|
|
x -= rise;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Tweener.addTween(this.actor, { opacity: 0,
|
|
x: x,
|
|
y: y,
|
|
transition: "linear",
|
|
time: POPUP_ANIMATION_TIME,
|
|
onComplete: Lang.bind(this, function () {
|
|
this.actor.hide();
|
|
this.actor.x = originalX;
|
|
this.actor.y = originalY;
|
|
if (onComplete)
|
|
onComplete();
|
|
})
|
|
});
|
|
},
|
|
|
|
_adjustAllocationForArrow: function(isWidth, alloc) {
|
|
let themeNode = this.actor.get_theme_node();
|
|
let borderWidth = themeNode.get_length('-arrow-border-width');
|
|
alloc.min_size += borderWidth * 2;
|
|
alloc.natural_size += borderWidth * 2;
|
|
if ((!isWidth && (this._arrowSide == St.Side.TOP || this._arrowSide == St.Side.BOTTOM))
|
|
|| (isWidth && (this._arrowSide == St.Side.LEFT || this._arrowSide == St.Side.RIGHT))) {
|
|
let rise = themeNode.get_length('-arrow-rise');
|
|
alloc.min_size += rise;
|
|
alloc.natural_size += rise;
|
|
}
|
|
},
|
|
|
|
_getPreferredWidth: function(actor, forHeight, alloc) {
|
|
let [minInternalSize, natInternalSize] = this.bin.get_preferred_width(forHeight);
|
|
alloc.min_size = minInternalSize;
|
|
alloc.natural_size = natInternalSize;
|
|
this._adjustAllocationForArrow(true, alloc);
|
|
},
|
|
|
|
_getPreferredHeight: function(actor, forWidth, alloc) {
|
|
let [minSize, naturalSize] = this.bin.get_preferred_height(forWidth);
|
|
alloc.min_size = minSize;
|
|
alloc.natural_size = naturalSize;
|
|
this._adjustAllocationForArrow(false, alloc);
|
|
},
|
|
|
|
_allocate: function(actor, box, flags) {
|
|
let themeNode = this.actor.get_theme_node();
|
|
let borderWidth = themeNode.get_length('-arrow-border-width');
|
|
let rise = themeNode.get_length('-arrow-rise');
|
|
let childBox = new Clutter.ActorBox();
|
|
let availWidth = box.x2 - box.x1;
|
|
let availHeight = box.y2 - box.y1;
|
|
|
|
childBox.x1 = 0;
|
|
childBox.y1 = 0;
|
|
childBox.x2 = availWidth;
|
|
childBox.y2 = availHeight;
|
|
this._border.allocate(childBox, flags);
|
|
|
|
childBox.x1 = borderWidth;
|
|
childBox.y1 = borderWidth;
|
|
childBox.x2 = availWidth - borderWidth;
|
|
childBox.y2 = availHeight - borderWidth;
|
|
switch (this._arrowSide) {
|
|
case St.Side.TOP:
|
|
childBox.y1 += rise;
|
|
break;
|
|
case St.Side.BOTTOM:
|
|
childBox.y2 -= rise;
|
|
break;
|
|
case St.Side.LEFT:
|
|
childBox.x1 += rise;
|
|
break;
|
|
case St.Side.RIGHT:
|
|
childBox.x2 -= rise;
|
|
break;
|
|
}
|
|
this.bin.allocate(childBox, flags);
|
|
},
|
|
|
|
_drawBorder: function(area) {
|
|
let themeNode = this.actor.get_theme_node();
|
|
|
|
let borderWidth = themeNode.get_length('-arrow-border-width');
|
|
let base = themeNode.get_length('-arrow-base');
|
|
let rise = themeNode.get_length('-arrow-rise');
|
|
let borderRadius = themeNode.get_length('-arrow-border-radius');
|
|
|
|
let halfBorder = borderWidth / 2;
|
|
let halfBase = Math.floor(base/2);
|
|
|
|
let borderColor = new Clutter.Color();
|
|
themeNode.get_color('-arrow-border-color', borderColor);
|
|
let backgroundColor = new Clutter.Color();
|
|
themeNode.get_color('-arrow-background-color', backgroundColor);
|
|
|
|
let [width, height] = area.get_surface_size();
|
|
let [boxWidth, boxHeight] = [width, height];
|
|
if (this._arrowSide == St.Side.TOP || this._arrowSide == St.Side.BOTTOM) {
|
|
boxHeight -= rise;
|
|
} else {
|
|
boxWidth -= rise;
|
|
}
|
|
let cr = area.get_context();
|
|
Clutter.cairo_set_source_color(cr, borderColor);
|
|
|
|
// Translate so that box goes from 0,0 to boxWidth,boxHeight,
|
|
// with the arrow poking out of that
|
|
if (this._arrowSide == St.Side.TOP) {
|
|
cr.translate(0, rise);
|
|
} else if (this._arrowSide == St.Side.LEFT) {
|
|
cr.translate(rise, 0);
|
|
}
|
|
|
|
let [x1, y1] = [halfBorder, halfBorder];
|
|
let [x2, y2] = [boxWidth - halfBorder, boxHeight - halfBorder];
|
|
|
|
cr.moveTo(x1 + borderRadius, y1);
|
|
if (this._arrowSide == St.Side.TOP) {
|
|
if (this._arrowCorner == St.Corner.TOPLEFT) {
|
|
cr.moveTo(x1, y1);
|
|
cr.lineTo(x1, y1 - rise);
|
|
cr.lineTo(x1 + halfBase, y1);
|
|
cr.lineTo(x2 - borderRadius, y1);
|
|
} else if (this._arrowCorner == St.Corner.TOPRIGHT) {
|
|
cr.lineTo(x2 - halfBase, y1);
|
|
cr.lineTo(x2, y1 - rise);
|
|
} else if (this._arrowOrigin < (x1 + (borderRadius + halfBase))) {
|
|
cr.lineTo(this._arrowOrigin, y1);
|
|
cr.lineTo(this._arrowOrigin, y1 - rise);
|
|
cr.lineTo(this._arrowOrigin + halfBase, y1);
|
|
cr.lineTo(x2 - borderRadius, y1);
|
|
} else if (this._arrowOrigin > (x2 - (borderRadius + halfBase))) {
|
|
cr.lineTo(this._arrowOrigin - halfBase, y1);
|
|
cr.lineTo(this._arrowOrigin, y1 - rise);
|
|
cr.lineTo(this._arrowOrigin, y1);
|
|
cr.lineTo(x2 - borderRadius, y1);
|
|
} else {
|
|
cr.lineTo(this._arrowOrigin - halfBase, y1);
|
|
cr.lineTo(this._arrowOrigin, y1 - rise);
|
|
cr.lineTo(this._arrowOrigin + halfBase, y1);
|
|
cr.lineTo(x2 - borderRadius, y1);
|
|
}
|
|
} else
|
|
cr.lineTo(x2 - borderRadius, y1);
|
|
|
|
// top-right corner
|
|
if (this._arrowCorner != St.Corner.TOPRIGHT)
|
|
cr.arc(x2 - borderRadius, y1 + borderRadius, borderRadius,
|
|
3*Math.PI/2, Math.PI*2);
|
|
|
|
if (this._arrowSide == St.Side.RIGHT) {
|
|
if (this._arrowCorner == St.Corner.TOPRIGHT) {
|
|
cr.lineTo(x2, y1);
|
|
cr.lineTo(x2 + rise, y1);
|
|
cr.lineTo(x2, y1 + halfBase);
|
|
cr.lineTo(x2, y2 - borderRadius);
|
|
} else if (this._arrowCorner == St.Corner.BOTTOMRIGHT) {
|
|
cr.moveTo(x2, y2 - halfBase);
|
|
cr.lineTo(x2 + rise, y2);
|
|
} else if (this._arrowOrigin < (y1 + (borderRadius + halfBase))) {
|
|
cr.lineTo(x2, this._arrowOrigin);
|
|
cr.lineTo(x2 + rise, this._arrowOrigin);
|
|
cr.lineTo(x2, this._arrowOrigin + halfBase);
|
|
cr.lineTo(x2, y2 - borderRadius);
|
|
} else if (this._arrowOrigin > (y2 - (borderRadius + halfBase))) {
|
|
cr.lineTo(x2, this._arrowOrigin - halfBase);
|
|
cr.lineTo(x2 + rise, this._arrowOrigin);
|
|
cr.lineTo(x2, this._arrowOrigin);
|
|
cr.lineTo(x2, y2 - borderRadius);
|
|
} else {
|
|
cr.lineTo(x2, this._arrowOrigin - halfBase);
|
|
cr.lineTo(x2 + rise, this._arrowOrigin);
|
|
cr.lineTo(x2, this._arrowOrigin + halfBase);
|
|
cr.lineTo(x2, y2 - borderRadius);
|
|
}
|
|
} else
|
|
cr.lineTo(x2, y2 - borderRadius);
|
|
|
|
// bottom-right corner
|
|
if (this._arrowCorner != St.Corner.BOTTOMRIGHT)
|
|
cr.arc(x2 - borderRadius, y2 - borderRadius, borderRadius,
|
|
0, Math.PI/2);
|
|
|
|
if (this._arrowSide == St.Side.BOTTOM) {
|
|
if (this._arrowCorner == St.Corner.BOTTOMLEFT) {
|
|
cr.lineTo(x1 + halfBase, y2);
|
|
cr.lineTo(x1, y2 + rise);
|
|
} else if (this._arrowCorner == St.Corner.BOTTOMRIGHT) {
|
|
cr.lineTo(x2, y2 + rise);
|
|
cr.lineTo(x2 - halfBase, y2);
|
|
cr.lineTo(x1 + borderRadius, y2);
|
|
} else if (this._arrowOrigin < (x1 + (borderRadius + halfBase))) {
|
|
cr.lineTo(this._arrowOrigin + halfBase, y2);
|
|
cr.lineTo(this._arrowOrigin, y2 + rise);
|
|
cr.lineTo(this._arrowOrigin, y2);
|
|
cr.lineTo(x1 + borderRadius, y2);
|
|
} else if (this._arrowOrigin > (x2 - (borderRadius + halfBase))) {
|
|
cr.lineTo(this._arrowOrigin, y2);
|
|
cr.lineTo(this._arrowOrigin, y2 + rise);
|
|
cr.lineTo(this._arrowOrigin - halfBase, y2);
|
|
cr.lineTo(x1 + borderRadius, y2);
|
|
} else {
|
|
cr.lineTo(this._arrowOrigin + halfBase, y2);
|
|
cr.lineTo(this._arrowOrigin, y2 + rise);
|
|
cr.lineTo(this._arrowOrigin - halfBase, y2);
|
|
cr.lineTo(x1 + borderRadius, y2);
|
|
}
|
|
} else
|
|
cr.lineTo(x1 + borderRadius, y2);
|
|
|
|
// bottom-left corner
|
|
if (this._arrowCorner != St.Corner.BOTTOMLEFT)
|
|
cr.arc(x1 + borderRadius, y2 - borderRadius, borderRadius,
|
|
Math.PI/2, Math.PI);
|
|
|
|
if (this._arrowSide == St.Side.LEFT) {
|
|
if (this._arrowCorner == St.Corner.TOPLEFT) {
|
|
cr.lineTo(x2, y1 + halfBase);
|
|
cr.lineTo(x1 - rise, y1);
|
|
} else if (this._arrowCorner == St.Corner.BOTTOMLEFT) {
|
|
cr.lineTo(x1 + rise, y2);
|
|
cr.moveTo(x1, y2 - halfBase);
|
|
} else if (this._arrowOrigin < (y1 + (borderRadius + halfBase))) {
|
|
cr.lineTo(x1, this._arrowOrigin + halfBase);
|
|
cr.lineTo(x1 - rise, this._arrowOrigin);
|
|
cr.lineTo(x1, this._arrowOrigin);
|
|
cr.lineTo(x1, y1 + borderRadius);
|
|
} else if (this._arrowOrigin > (y2 - (borderRadius + halfBase))) {
|
|
cr.lineTo(x1, this._arrowOrigin);
|
|
cr.lineTo(x1 - rise, this._arrowOrigin);
|
|
cr.lineTo(x1, this._arrowOrigin - halfBase);
|
|
cr.lineTo(x1, y1 + borderRadius);
|
|
} else {
|
|
cr.lineTo(x1, this._arrowOrigin + halfBase);
|
|
cr.lineTo(x1 - rise, this._arrowOrigin);
|
|
cr.lineTo(x1, this._arrowOrigin - halfBase);
|
|
cr.lineTo(x1, y1 + borderRadius);
|
|
}
|
|
} else
|
|
cr.lineTo(x1, y1 + borderRadius);
|
|
|
|
// top-left corner
|
|
if (this._arrowCorner != St.Corner.TOPLEFT)
|
|
cr.arc(x1 + borderRadius, y1 + borderRadius, borderRadius,
|
|
Math.PI, 3*Math.PI/2);
|
|
else
|
|
cr.lineTo(x1, y1);
|
|
|
|
Clutter.cairo_set_source_color(cr, backgroundColor);
|
|
cr.fillPreserve();
|
|
Clutter.cairo_set_source_color(cr, borderColor);
|
|
cr.setLineWidth(borderWidth);
|
|
cr.stroke();
|
|
},
|
|
|
|
setPosition: function(sourceActor, gap, alignment) {
|
|
// We need to show it now to force an allocation,
|
|
// so that we can query the correct size.
|
|
this.actor.show();
|
|
|
|
// Position correctly relative to the sourceActor
|
|
let [sourceX, sourceY] = sourceActor.get_transformed_position();
|
|
let [sourceWidth, sourceHeight] = sourceActor.get_transformed_size();
|
|
let [sourceCenterX, sourceCenterY] = [sourceX + (sourceWidth / 2), sourceY + (sourceHeight / 2)];
|
|
let [minWidth, minHeight, natWidth, natHeight] = this.actor.get_preferred_size();
|
|
|
|
// We also want to keep it onscreen, and separated from the
|
|
// edge by the same distance as the main part of the box is
|
|
// separated from its sourceActor
|
|
let primary = global.get_primary_monitor();
|
|
let themeNode = this.actor.get_theme_node();
|
|
let halfBorder = themeNode.get_length('-arrow-border-width') / 2;
|
|
let halfBase = themeNode.get_length('-arrow-base') / 2;
|
|
let borderRadius = themeNode.get_length('-arrow-border-radius');
|
|
|
|
let margin = 2 * borderRadius + halfBorder;
|
|
|
|
let resX, resY;
|
|
this._arrowCorner = null;
|
|
|
|
switch (this._arrowSide) {
|
|
case St.Side.TOP:
|
|
resY = sourceY + sourceHeight + gap;
|
|
break;
|
|
case St.Side.BOTTOM:
|
|
resY = sourceY - natHeight - gap;
|
|
break;
|
|
case St.Side.LEFT:
|
|
resX = sourceX + sourceWidth + gap;
|
|
break;
|
|
case St.Side.RIGHT:
|
|
resX = sourceX - natWidth - gap;
|
|
break;
|
|
}
|
|
|
|
// Now align and position the pointing axis, making sure
|
|
// it fits on screen
|
|
switch (this._arrowSide) {
|
|
case St.Side.TOP:
|
|
case St.Side.BOTTOM:
|
|
switch (alignment) {
|
|
case St.Align.START:
|
|
resX = sourceCenterX - (halfBase + borderRadius + halfBorder);
|
|
break;
|
|
case St.Align.MIDDLE:
|
|
resX = sourceCenterX - (natWidth / 2);
|
|
break;
|
|
case St.Align.END:
|
|
resX = sourceCenterX - natWidth + (halfBase + borderRadius + halfBorder);
|
|
break;
|
|
}
|
|
if (sourceCenterX < margin) {
|
|
// Not enough space to the top
|
|
this._arrowCorner = (this._arrowSide == St.Side.TOP) ? St.Corner.TOPLEFT : St.Corner.BOTTOMLEFT;
|
|
resX = 10;
|
|
} else if (sourceCenterX > (primary.width - margin)) {
|
|
// Not enough space to the botom
|
|
this._arrowCorner = (this._arrowSide == St.Side.TOP) ? St.Corner.TOPRIGHT : St.Corner.BOTTOMRIGHT;
|
|
resX = primary.width - (10 + natWidth);
|
|
}
|
|
|
|
resX = Math.max(resX, 10);
|
|
resX = Math.min(resX, primary.width - (10 + natWidth));
|
|
|
|
this.setArrowOrigin(sourceCenterX - resX);
|
|
break;
|
|
|
|
case St.Side.LEFT:
|
|
case St.Side.RIGHT:
|
|
switch (alignment) {
|
|
case St.Align.START:
|
|
resY = sourceCenterY - (halfBase + borderRadius + halfBorder);
|
|
break;
|
|
case St.Align.MIDDLE:
|
|
resY = sourceCenterY - (natHeight / 2);
|
|
break;
|
|
case St.Align.END:
|
|
resY = sourceCenterY - natHeight + (halfBase + borderRadius + halfBorder);
|
|
break;
|
|
}
|
|
if (sourceCenterY < margin) {
|
|
// Not enough space to the left
|
|
this._arrowCorner = (this._arrowSide == St.Side.LEFT) ? St.Corner.TOPLEFT : St.Corner.TORIGHT;
|
|
resY = 10;
|
|
}
|
|
else if (sourceCenterY > (primary.height - margin)) {
|
|
// Not enough space to the right
|
|
this._arrowCorner = (this._arrowSide == St.Side.LEFT) ? St.Corner.BOTTOMLEFT : St.Corner.BOTTOMRIGHT;
|
|
resY = primary.height - (10 + natHeight);
|
|
}
|
|
|
|
resY = Math.max(resY, 10);
|
|
resY = Math.min(resY, primary.height - (10 + natHeight));
|
|
|
|
this.setArrowOrigin(sourceCenterY - resY);
|
|
break;
|
|
}
|
|
|
|
let parent = this.actor.get_parent();
|
|
let success, x, y;
|
|
while (!success) {
|
|
[success, x, y] = parent.transform_stage_point(resX, resY);
|
|
parent = parent.get_parent();
|
|
}
|
|
|
|
// Actually set the position
|
|
this.actor.x = Math.floor(x);
|
|
this.actor.y = Math.floor(y);
|
|
},
|
|
|
|
// @origin: Coordinate specifying middle of the arrow, along
|
|
// the Y axis for St.Side.LEFT, St.Side.RIGHT from the top and X axis from
|
|
// the left for St.Side.TOP and St.Side.BOTTOM.
|
|
setArrowOrigin: function(origin) {
|
|
if (this._arrowOrigin != origin) {
|
|
this._arrowOrigin = origin;
|
|
this._border.queue_repaint();
|
|
}
|
|
}
|
|
};
|