/* -*- 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 = primary.x + 10; } else if (sourceCenterX > (primary.x + primary.width - margin)) { // Not enough space to the botom this._arrowCorner = (this._arrowSide == St.Side.TOP) ? St.Corner.TOPRIGHT : St.Corner.BOTTOMRIGHT; resX = primary.x + primary.width - (10 + natWidth); } resX = Math.max(resX, primary.x + 10); resX = Math.min(resX, primary.x + 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.y + primary.height - margin)) { // Not enough space to the right this._arrowCorner = (this._arrowSide == St.Side.LEFT) ? St.Corner.BOTTOMLEFT : St.Corner.BOTTOMRIGHT; resY = primary.y + primary.height - (10 + natHeight); } resY = Math.max(resY, primary.y + 10); resY = Math.min(resY, primary.y + 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(); } } };