PopupMenu: add horizontal sliders

Introduce Cairo-drawn sliders to be used in PopupMenus (for example for
volume). They are stylable to some extent (colors, border width, slider
height) and have the standard behaviour of a slider, except they are
completely modal (once you start dragging, all events are intercepted by
the slider, which thus is kept active and highlighted at all times).
They show numeric values between 0 and 1 (scaling must be performed outside)
and emit value-changed on button release, but no activate, keeping the
menu open.

https://bugzilla.gnome.org/show_bug.cgi?id=625029
This commit is contained in:
Giovanni Campagna 2010-07-22 14:34:02 +02:00 committed by Dan Winship
parent aebdf79430
commit f336564e19
2 changed files with 168 additions and 1 deletions

View File

@ -148,6 +148,15 @@ StTooltip {
height: 1em;
}
.popup-slider-menu-item {
height: 1em;
-slider-height: 0.3em;
-slider-background-color: #333333;
-slider-border-color: #5f5f5f;
-slider-border-width: 1px;
-slider-handle-radius: 0.5em;
}
/* Switches (to be used in menus) */
.toggle-switch {
width: 4.5em;

View File

@ -4,8 +4,9 @@ const Cairo = imports.cairo;
const Clutter = imports.gi.Clutter;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const St = imports.gi.St;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const St = imports.gi.St;
const BoxPointer = imports.ui.boxpointer;
const Main = imports.ui.main;
@ -164,6 +165,163 @@ PopupSeparatorMenuItem.prototype = {
}
};
function PopupSliderMenuItem() {
this._init.apply(this, arguments);
}
PopupSliderMenuItem.prototype = {
__proto__: PopupBaseMenuItem.prototype,
_init: function(value) {
PopupBaseMenuItem.prototype._init.call(this, { activate: false });
if (isNaN(value))
// Avoid spreading NaNs around
throw TypeError('The slider value must be a number');
this._displayValue = this._value = Math.max(Math.min(value, 1), 0);
this._slider = new St.DrawingArea({ style_class: 'popup-slider-menu-item', reactive: true });
this.actor.set_child(this._slider);
this._slider.connect('repaint', Lang.bind(this, this._sliderRepaint));
this._slider.connect('button-press-event', Lang.bind(this, this._startDragging));
this._releaseId = this._motionId = 0;
this._dragging = false;
},
setValue: function(value) {
if (isNaN(value))
throw TypeError('The slider value must be a number');
this._displayValue = this._value = Math.max(Math.min(value, 1), 0);
this._slider.queue_repaint();
},
_sliderRepaint: function(area) {
let cr = area.get_context();
let themeNode = area.get_theme_node();
let [width, height] = area.get_surface_size();
let found, handleRadius;
[found, handleRadius] = themeNode.get_length('-slider-handle-radius', false);
let sliderWidth = width - 2 * handleRadius;
let sliderHeight;
[found, sliderHeight] = themeNode.get_length('-slider-height', false);
let sliderBorderWidth;
[found, sliderBorderWidth] = themeNode.get_length('-slider-border-width', false);
let sliderBorderColor = new Clutter.Color();
themeNode.get_color('-slider-border-color', false, sliderBorderColor);
let sliderColor = new Clutter.Color();
themeNode.get_color('-slider-background-color', false, sliderColor);
cr.setSourceRGBA (
sliderColor.red / 255,
sliderColor.green / 255,
sliderColor.blue / 255,
sliderColor.alpha / 255);
cr.rectangle(handleRadius, (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();
let handleY = height / 2;
let handleX = handleRadius + (width - 2 * handleRadius) * this._displayValue;
let color = new Clutter.Color();
themeNode.get_foreground_color(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();
},
_startDragging: function(actor, event) {
if (this._dragging) // don't allow two drags at the same time
return;
this._dragging = true;
// 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));
let absX, absY;
[absX, absY] = event.get_coords();
this._moveHandle(absX, absY);
},
_endDragging: function() {
if (this._dragging) {
this._slider.disconnect(this._releaseId);
this._slider.disconnect(this._motionId);
Clutter.ungrab_pointer();
this._dragging = false;
this._value = this._displayValue;
this.emit('value-changed', this._value);
}
return true;
},
_motionEvent: function(actor, event) {
let absX, absY;
[absX, absY] = event.get_coords();
this._moveHandle(absX, absY)
return true;
},
_moveHandle: function(absX, absY) {
let relX, relY, sliderX, sliderY;
[sliderX, sliderY] = this._slider.get_transformed_position();
relX = absX - sliderX;
relY = absY - sliderY;
let width = this._slider.width;
let found, handleRadius;
[found, handleRadius] = this._slider.get_theme_node().get_length('-slider-handle-radius', false);
let newvalue;
if (relX < handleRadius)
newvalue = 0;
else if (relX > width - handleRadius)
newvalue = 1;
else
newvalue = (relX - handleRadius) / (width - 2 * handleRadius);
this._displayValue = newvalue;
this._slider.queue_repaint();
},
get value() {
return this._value;
},
handleKeyPress: function(event) {
let key = event.get_key_symbol();
if (key == Clutter.Right || key == Clutter.Left) {
let delta = key == Clutter.Right ? 0.1 : -0.1;
this._value = this._displayValue = Math.max(0, Math.min(this._value + delta, 1));
this._slider.queue_repaint();
this.emit('value-changed', this._value);
return true;
}
return false;
}
}
function PopupSwitchMenuItem() {
this._init.apply(this, arguments);
}