Skip to content

Instantly share code, notes, and snippets.

@mathematicalcoffee
Created December 12, 2012 00:01
Show Gist options
  • Save mathematicalcoffee/4263565 to your computer and use it in GitHub Desktop.
Save mathematicalcoffee/4263565 to your computer and use it in GitHub Desktop.
Some javascript classes for gnome-shell extensions that create handy Popup Menu Items. PopupDoubleSliderMenuItem: a PopupSliderMenuItem with two sliders, for a lower and upper limit. PopupSliderMenuItemWithLabel: a PopupSliderMenuItem with a convenience label showing the current value. You can specify the lower/upper limits of the slider.
/* A SliderMenuItem with two slidable things, for
* selecting a range. Basically a modified PopupSliderMenuItem.
* It has no scroll or key-press event as it's hard to tell which
* blob the user meant to scroll.
*/
function PopupDoubleSliderMenuItem() {
this._init.apply(this, arguments);
}
PopupDoubleSliderMenuItem.prototype = {
__proto__: PopupMenu.PopupBaseMenuItem.prototype,
_init: function (val1, val2) {
PopupMenu.PopupBaseMenuItem.prototype._init.call(this,
{ activate: false });
if (isNaN(val1) || isNaN(val2))
// Avoid spreading NaNs around
throw new TypeError('The slider value must be a number');
this._values = [Math.max(Math.min(val1, 1), 0),
Math.max(Math.min(val2, 1), 0)];
this._slider = new St.DrawingArea({
style_class: 'popup-slider-menu-item',
reactive: true
});
this.addActor(this._slider, { span: -1, expand: true });
this._slider.connect('repaint', Lang.bind(this, this._sliderRepaint));
this.actor.connect('button-press-event', Lang.bind(this,
this._startDragging));
this._releaseId = this._motionId = 0;
this._dragging = false;
},
_setValue: function (i, value) {
if (isNaN(value))
throw new TypeError('The slider value must be a number');
this._value[i] = Math.max(Math.min(value, 1), 0);
this._slider.queue_repaint();
},
setValue1: function (value) {
this._setValue(0, value);
},
setValue2: function (value) {
this._setValue(1, value);
},
_sliderRepaint: function (area) {
let cr = area.get_context();
let themeNode = area.get_theme_node();
let [width, height] = area.get_surface_size();
let handleRadius = themeNode.get_length('-slider-handle-radius');
let sliderWidth = width - 2 * handleRadius;
let sliderHeight = themeNode.get_length('-slider-height');
let sliderBorderWidth = themeNode.get_length('-slider-border-width');
let sliderBorderColor = themeNode.get_color('-slider-border-color');
let sliderColor = themeNode.get_color('-slider-background-color');
let sliderActiveBorderColor = themeNode.get_color('-slider-active-border-color');
let sliderActiveColor = themeNode.get_color('-slider-active-background-color');
/* slider active colour from val0 to val1 */
cr.setSourceRGBA(
sliderActiveColor.red / 255,
sliderActiveColor.green / 255,
sliderActiveColor.blue / 255,
sliderActiveColor.alpha / 255);
cr.rectangle(handleRadius + sliderWidth * this._values[0],
(height - sliderHeight) / 2,
sliderWidth * this._values[1], sliderHeight);
cr.fillPreserve();
cr.setSourceRGBA(
sliderActiveBorderColor.red / 255,
sliderActiveBorderColor.green / 255,
sliderActiveBorderColor.blue / 255,
sliderActiveBorderColor.alpha / 255);
cr.setLineWidth(sliderBorderWidth);
cr.stroke();
/* slider from 0 to val0 */
cr.setSourceRGBA(
sliderColor.red / 255,
sliderColor.green / 255,
sliderColor.blue / 255,
sliderColor.alpha / 255);
cr.rectangle(handleRadius, (height - sliderHeight) / 2,
sliderWidth * this._values[0], sliderHeight);
cr.fillPreserve();
cr.setSourceRGBA(
sliderBorderColor.red / 255,
sliderBorderColor.green / 255,
sliderBorderColor.blue / 255,
sliderBorderColor.alpha / 255);
cr.setLineWidth(sliderBorderWidth);
cr.stroke();
/* slider from val1 to 1 */
cr.setSourceRGBA(
sliderColor.red / 255,
sliderColor.green / 255,
sliderColor.blue / 255,
sliderColor.alpha / 255);
cr.rectangle(handleRadius + sliderWidth * this._values[1],
(height - sliderHeight) / 2,
sliderWidth, sliderHeight);
cr.fillPreserve();
cr.setSourceRGBA(
sliderBorderColor.red / 255,
sliderBorderColor.green / 255,
sliderBorderColor.blue / 255,
sliderBorderColor.alpha / 255);
cr.setLineWidth(sliderBorderWidth);
cr.stroke();
/* dots */
let i = this._values.length;
while (i--) {
let val = this._values[i];
let handleY = height / 2;
let handleX = handleRadius + (width - 2 * handleRadius) * val;
let color = themeNode.get_foreground_color();
cr.setSourceRGBA(
color.red / 255,
color.green / 255,
color.blue / 255,
color.alpha / 255);
cr.arc(handleX, handleY, handleRadius, 0, 2 * Math.PI);
cr.fill();
}
},
/* returns the index of the dot to move */
_whichDotToMove: function (absX, absY) {
let relX, relY, sliderX, sliderY;
[sliderX, sliderY] = this._slider.get_transformed_position();
relX = absX - sliderX;
let width = this._slider.width,
handleRadius = this._slider.get_theme_node().get_length(
'-slider-handle-radius'),
newvalue;
if (relX < handleRadius)
newvalue = 0;
else if (relX > width - handleRadius)
newvalue = 1;
else
newvalue = (relX - handleRadius) / (width - 2 * handleRadius);
return (Math.abs(newvalue - this._values[0]) <
Math.abs(newvalue - this._values[1]) ? 0 : 1);
},
_endDragging: function () {
PopupMenu.PopupSliderMenuItem.prototype._endDragging.apply(this, arguments);
},
_startDragging: function (actor, event) {
if (this._dragging) // don't allow two drags at the same time
return;
this._dragging = true;
let absX, absY;
[absX, absY] = event.get_coords();
let dot = this._whichDotToMove(absX, absY);
// FIXME: we should only grab the specific device that originated
// the event, but for some weird reason events are still delivered
// outside the slider if using clutter_grab_pointer_for_device
Clutter.grab_pointer(this._slider);
this._releaseId = this._slider.connect('button-release-event',
Lang.bind(this, this._endDragging));
this._motionId = this._slider.connect('motion-event',
Lang.bind(this, this._motionEvent, dot));
this._moveHandle(absX, absY, dot);
},
_motionEvent: function (actor, event, dot) {
let absX, absY;
[absX, absY] = event.get_coords();
this._moveHandle(absX, absY, dot);
return true;
},
/* Don't let the bottom slider cross over the top slider
* and vice versa */
_moveHandle: function (absX, absY, which) {
let relX, relY, sliderX, sliderY;
[sliderX, sliderY] = this._slider.get_transformed_position();
relX = absX - sliderX;
relY = absY - sliderY;
let width = this._slider.width,
handleRadius = this._slider.get_theme_node().get_length(
'-slider-handle-radius'),
newvalue = (relX - handleRadius) / (width - 2 * handleRadius);
newvalue = Math.max(which ? this._values[0] : 0,
Math.min(newvalue, which ? 1 : this._values[1]));
this._values[which] = newvalue;
this._slider.queue_repaint();
this.emit('value-changed', this._values[which], which);
}
};
/* A slider with a label + number that updates with the slider.
* The range of the slider may be set from `min` to `max`.
* Use getValue() and setValue(val) to get/set the value of the slider (between `min` and `max`).
* Use getValue(true) and setValue(val, true) to get/set the value of the slider on the underlying 0 to 1 scale.
*
* text: the text for the item
* defaultVal: the intial value for the item (on the min -> max scale)
* min, max: the min and max values for the slider
* round: whether to round the value to the nearest integer
* ndec: number of decimal places to round to
* params: other params for PopupBaseMenuItem
*
*/
function PopupSliderMenuItemWithLabel() {
this._init.apply(this, arguments);
}
PopupSliderMenuItemWithLabel.prototype = {
__proto__: PopupMenu.PopupBaseMenuItem.prototype,
_init: function (text, defaultVal, min, max, round, ndec, params) {
PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);
/* set up properties */
this.min = min || 0;
this.max = max || 1;
this.round = round || false;
this._value = defaultVal;
if (round) {
this._value = Math.round(this._value);
}
this.ndec = this.ndec || (round ? 0 : 2);
/* set up item */
this.box = new St.BoxLayout({vertical: true});
this.addActor(this.box, {expand: true, span: -1});
this.topBox = new St.BoxLayout({vertical: false,
style_class: 'slider-menu-item-top-box'});
this.box.add(this.topBox, {x_fill: true});
this.bottomBox = new St.BoxLayout({vertical: false,
style_class: 'slider-menu-item-bottom-box'});
this.box.add(this.bottomBox, {x_fill: true});
/* text */
this.label = new St.Label({text: text, reactive: false});
/* number */
this.numberLabel = new St.Label({text: this._value.toFixed(this.ndec),
reactive: false});
/* slider */
this.slider = new PopupMenu.PopupSliderMenuItem((defaultVal - min) /
(max - min)); // between 0 and 1
/* connect up signals */
this.slider.connect('value-changed',
Lang.bind(this, this._updateValue));
/* pass through the drag-end, clicked signal */
this.slider.connect('drag-end', Lang.bind(this, function () {
this.emit('drag-end', this._value);
}));
// Note: if I set the padding in the css it gets overridden
this.slider.actor.set_style('padding-left: 0em; padding-right: 0em;');
/* assemble the item */
this.topBox.add(this.label, {expand: true});
this.topBox.add(this.numberLabel, {align: St.Align.END});
this.bottomBox.add(this.slider.actor, {expand: true, span: -1});
},
/* returns the value of the slider, either the raw (0-1) value or the
* value on the min->max scale. */
getValue: function (raw) {
if (raw) {
return this.slider.value;
} else {
return this._value;
}
},
/* sets the value of the slider, either the raw (0-1) value or the
* value on the min->max scale */
setValue: function (value, raw) {
value = (raw ? value : (value - this.min) / (this.max - this.min));
this._updateValue(this.slider, value);
this.slider.setValue(value);
},
_updateValue: function (slider, value) {
let val = value * (this.max - this.min) + this.min;
if (this.round) {
val = Math.round(val);
}
this._value = val;
this.numberLabel.set_text(val.toFixed(this.ndec));
},
};
/* For PopupSliderMenuItemWithLabel */
.slider-menu-item-top-box {
padding-left: 0em;
padding-right: 1.75em;
}
.slider-menu-item-bottom-box {
padding-left: 0em;
padding-right: 0em;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment