From f336564e19dbd28ec382f804eeed25e7ad22305e Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Thu, 22 Jul 2010 14:34:02 +0200 Subject: [PATCH] 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 --- data/theme/gnome-shell.css | 9 +++ js/ui/popupMenu.js | 160 ++++++++++++++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 1 deletion(-) diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 0e96be811..3c63794bf 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -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; diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js index de8de1eca..7e60fe821 100644 --- a/js/ui/popupMenu.js +++ b/js/ui/popupMenu.js @@ -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); }