Commits

mathematicalcoffee committed 14b36a3 Merge

merged dev bookmark to gnome3.2; this is v2.0.

Comments (0)

Files changed (10)

+v2.0:
+* many changes allowing XPenguins to run in a window
+* probably more, but I didn't document them :(
 v1.1:
 * use windowListener class from the window-HUD repository
 * removed ignoreHalfMaximised. It doesn't make sense.
 # Options.
 To start/stop XPenguins, use the toggle.
 
+### Run in a particular window
+Click the "Running in:" item and choose which window you want the XPenguins to run around in ("Cancel" to have them use the desktop).
+
 ### Theme
 XPenguins is themable, and you can have multiple themes running simultaneously. 
 Use the sliders to add or remove toons from each theme.
 ### Ignore maximised windows
 Whether maximised windows (and windows underneath these) are ignored.
 Toons only run around on the region of your desktop not covered by windows, so if windows are maximised you won't get any toons. Ignoring maximised windows means you can enjoy toons even with maximised windows. They will only bump into windows that are visible and non-maximised.
-### Ignore half-maximised windows
-If ignoring maximised windows is true, should we also ignore half-maximised windows (those that are maximised vertically or horizontally but not both)? This can be handy for people with big monitors who just use half-maximised windows all the time. **Only used if 'Ignore Maximised' is true.**
 ### Always on visible workspace
 Whether toons stay on the workspace you started XPenguins on, or follow you around all your workspaces.
+(Not applicable in windowed mode; then this setting is taken from the window itself).
 ### Blood
 Whether to show animations with blood (for example in the original Penguin theme the 'splat' animation has blood).
 ### Angels
 Enabling "God mode" lets you squash (smite) toons by clicking on them.
 ### Time between frames
 The time (in milliseconds) between each frame of the animation. By default the number specified by the theme is used (probably 60ms).
-### Recalc Mode
+### Recalc Mode (GNOME 3.4+)
 Ignore this. If you are suffering from severe performance issues AND you resize/move your windows really really often you can try switching this to "PAUSE" which will pause toons while you drag windows around. It probably won't make much difference at all.
 ### Load Averaging
 This defines two thresholds; when the computer's load average (for example given by `uptime` or `top`) exceeds the lower threshold, toons will start to be killed. 
 Alternatively (developers?):
 
 1. Checkout the repository: `hg clone https://bitbucket.org/mathematicalcoffee/xpenguins-gnome-shell-extension`
-2. Update to the `3.2` or `3.4` branch (the `default` branch is **NOT** guaranteed to work!).
+2. Update to the `gnome3.2` or `gnome3.4` branch (the `default` branch is **NOT** guaranteed to work!).
 3. Copy the folder `xpenguins@mathematical.coffee.gmail.com` to `.local/share/gnome-shell/extensions`.
 4. If on GNOME 3.2, use `dconf-editor` and modify the key `/org/gnome/shell/enabled-extensions` to include `'xpenguins@mathematical.coffee.gmail.com'`. 
 If on GNOME 3.4, then just do `gnome-shell-extension-tool -e xpenguins@mathematical.coffee.gmail.com`.
 
 * When you have dual monitors of different sizes, toons will happily walk in the areas of the combined screens' bounding box that are not visible to the user (patches welcome!).
 * Toons don't treat the message tray or gnome-shell menus/popups as solid. This is because XPenguins can only make toons interact with objects that the window manager knows about, and things created with GNOME-shell such as the message tray/notifications are not handled by the window manager.
+* When you first start XPenguins or first add in toons from another theme, you get an annoying flicker where all the toons' pixmaps are drawn on the screen very briefly before they get hidden.
 
 Windowed mode is much harder than desktop mode, and as such has some caveats:
 
 
 - toons talk to each other with little speech bubbles
 - toons jump up and down when you get new mail
-- toons try to get to your cursor (???) --> climb up windows/turn into superman/etc in order to get there?
+- see [issues page](https://bitbucket.org/mathematicalcoffee/xpenguins-gnome-shell-extension/issues?status=new&status=open) for more.
 
 ---
 # Changelog

xpenguins@mathematical.coffee.gmail.com/extension.js

 /* *** CODE *** */
-const Clutter  = imports.gi.Clutter;
 const Gio      = imports.gi.Gio;
 const GLib     = imports.gi.GLib;
-const Gtk      = imports.gi.Gtk;
 const Lang     = imports.lang;
-const Mainloop = imports.mainloop;
-const Meta      = imports.gi.Meta;
-const Pango    = imports.gi.Pango;
-const St       = imports.gi.St;
 
-const AltTab    = imports.ui.altTab;
 const Main      = imports.ui.main;
-const ModalDialog = imports.ui.modalDialog;
 const PanelMenu = imports.ui.panelMenu;
 const PopupMenu = imports.ui.popupMenu;
 
-const Gettext = imports.gettext.domain('gnome-shell-extensions');
+const Gettext = imports.gettext.domain('xpenguins');
 const _ = Gettext.gettext;
 
 /* my files */
 const Me = imports.ui.extensionSystem.extensions['xpenguins@mathematical.coffee.gmail.com'];
 const ThemeManager = Me.themeManager;
+const UI = Me.ui;
 const WindowListener = Me.windowListener;
 const XPenguins = Me.xpenguins;
 const XPUtil = Me.util;
 
 //// Classes ////
 
-function WindowPickerDialog() {
-    this._init.apply(this, arguments);
-}
-
-WindowPickerDialog.prototype = {
-    __proto__: ModalDialog.ModalDialog.prototype,
-
-    _init: function () {
-        ModalDialog.ModalDialog.prototype._init.call(this,
-            {styleClass: 'modal-dialog'});
-
-        let monitor = global.screen.get_monitor_geometry(global.screen.get_primary_monitor()),
-            width   = Math.round(monitor.width * .6),
-            height  = Math.min(300, Math.round(monitor.height * .3));
-
-        /* title + icon */
-        let box = new St.BoxLayout();
-        box.add(new St.Label({text: _("Select which window to run XPenguins in, or 'Cancel' to use the Desktop:")}),
-                    {x_fill: true});
-        this.contentLayout.add(box, {x_fill: true});
-
-        /* scroll box */
-        this.scrollBox = new St.ScrollView({
-            x_fill: true,
-            y_fill: true,
-            width: width,
-            height: height
-        });
-        // automatic horizontal scrolling, no vertical scrolling
-        this.scrollBox.set_policy(Gtk.PolicyType.AUTOMATIC,
-            Gtk.PolicyType.NEVER);
-
-        /* thumbnails in scroll box (put in BoxLayout or else cannot see) */
-        let box = new St.BoxLayout();
-        this._windows = global.get_window_actors().map(function (w) {
-            return w.meta_window;
-        });
-        /* filter out Nautilus desktop window */
-        this._windows = this._windows.filter(function (w) {
-            return w.window_type != Meta.WindowType.DESKTOP;
-        });
-        this._thumbnails = new AltTab.ThumbnailList(this._windows);
-        this._thumbnails.actor.get_allocation_box();
-        box.add(this._thumbnails.actor, {expand: true, x_fill: true, y_fill: true});
-        this.scrollBox.add_actor(box,
-            {expand: true, x_fill: true, y_fill: true});
-        this.contentLayout.add(this.scrollBox, {expand: true, x_fill: true, y_fill: true});
-        // need to call addClones at some point. it was called in _allocate ...
-
-        /* Cancel button */
-        this.setButtons([{
-            label: _("Cancel"),
-            action: Lang.bind(this, function () {
-                this._windowActivated(this._thumbnails, -1);
-            })
-        }]);
-	},
-
-    open: function() {
-        ModalDialog.ModalDialog.prototype.open.apply(this, arguments);
-        this._thumbnails.addClones(this.scrollBox.height);
-        this._thumbnails.connect('item-activated', Lang.bind(this, this._windowActivated));
-        this._thumbnails.connect('item-entered', Lang.bind(this, this._windowEntered));
-    },
-
-    _windowActivated: function (thumbnails, n) {
-        this.emit('window-selected', this._windows[n]);
-        this.close(global.get_current_time());
-    },
-
-    _windowEntered: function (thumbnails, n) {
-        this._thumbnails.highlight(n);
-    }
-};
-
-/* Popup dialog with scrollable text.
- * See InstallExtensionDialog in extensionSystem.js for an example.
- *
- * Future icing: make one toon of each type in the theme and have them run
- * in the about dialog.
- */
-function AboutDialog() {
-    this._init.apply(this, arguments);
-}
-
-AboutDialog.prototype = {
-    __proto__: ModalDialog.ModalDialog.prototype,
-
-    _init: function (title, text, icon_path) {
-        ModalDialog.ModalDialog.prototype._init.call(this,
-            {styleClass: 'modal-dialog'});
-
-        let monitor = global.screen.get_monitor_geometry(global.screen.get_primary_monitor()),
-            width   = Math.max(400, Math.round(monitor.width / 3)),
-            height  = Math.max(400, Math.round(monitor.height / 2.5));
-
-        /* title + icon */
-        this.titleBox = new St.BoxLayout({vertical: false});
-        this.contentLayout.add(this.titleBox,
-            {x_fill: false, x_align: St.Align.MIDDLE});
-
-        this.icon = new St.Icon({
-            icon_name: 'image-missing',
-            icon_type: St.IconType.FULLCOLOR,
-            style_class: 'xpenguins-about-icon'
-        });
-        this.setIcon(icon_path);
-        this.titleBox.add(this.icon);
-
-        this.title = new St.Label({text: title || '',
-            style_class: 'xpenguins-about-title'});
-        this.titleBox.add(this.title,  {x_fill: true});
-
-        /* scroll box */
-        this.scrollBox = new St.ScrollView({
-            x_fill: true,
-            y_fill: true,
-            width: width,
-            height: height
-        });
-        // automatic horizontal scrolling, automatic vertical scrolling
-        this.scrollBox.set_policy(Gtk.PolicyType.AUTOMATIC,
-            Gtk.PolicyType.AUTOMATIC);
-
-        /* text in scrollbox.
-         * For some reason it won't display unless in a St.BoxLayout. */
-        this.text = new St.Label({text: (text || ''),
-            style_class: 'xpenguins-about-text'});
-        this.text.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; // allows scrolling
-        //this.text.clutter_text.line_wrap = true;
-
-        this.box = new St.BoxLayout();
-        this.box.add(this.text, { expand: true });
-        this.scrollBox.add_actor(this.box,
-            {expand: true, x_fill: true, y_fill: true});
-        this.contentLayout.add(this.scrollBox,
-            {expand: true, x_fill: true, y_fill: true});
-
-        /* OK button */
-        this.setButtons([{
-            label: _("OK"),
-            action: Lang.bind(this, function () {
-                this.close(global.get_current_time());
-            })
-        }]);
-	},
-
-    setTitle: function (title) {
-        this.title.text = title;
-    },
-
-    setText: function (text) {
-        this.text.text = text;
-    },
-
-    appendText: function (text, sep) {
-        this.text.text += (sep || '\n') + text;
-    },
-
-    setIcon: function (icon_path) {
-        let path = icon_path ? Gio.file_new_for_path(icon_path) : null;
-        if (path && path.query_exists(null)) {
-            this.icon.set_gicon(new Gio.FileIcon({file: path}));
-        }
-    }
-};
-
-/* A DoubleSliderPopupMenuItem paired with a text label & two number labels */
-function DoubleSliderMenuItem() {
-    this._init.apply(this, arguments);
-}
-
-DoubleSliderMenuItem.prototype = {
-    __proto__: PopupMenu.PopupBaseMenuItem.prototype,
-
-    _init: function (text, valLower, valUpper, 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._values = [valLower, valUpper];
-        this._numVals = this._values.length; // pre-cache
-        if (round) {
-            this._values = this._values.map(function (v) {
-                return Math.round(v);
-            });
-        }
-        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: 'double-slider-menu-item-top-box'});
-        this.box.add(this.topBox, {x_fill: true});
-
-        this.bottomBox = new St.BoxLayout({vertical: false, 
-            style_class: 'double-slider-menu-item-bottom-box'});
-        this.box.add(this.bottomBox, {x_fill: true});
-
-        /* text */
-        this.label = new St.Label({text: text, reactive: false,
-            style_class: 'double-slider-menu-item-label'});
-
-        /* numbers */
-        this.numberLabelLower = new St.Label({text: this._values[0].toFixed(this.ndec), 
-            reactive: false});
-        this.numberLabelUpper = new St.Label({text: this._values[1].toFixed(this.ndec), 
-            reactive: false});
-        this.numberLabelLower.add_style_class_name('double-slider-menu-item-number-label');
-        this.numberLabelUpper.add_style_class_name('double-slider-menu-item-number-label');
-
-        /* slider */
-        this.slider = new DoubleSliderPopupMenuItem(
-            (valLower - min) / (max - min),
-            (valUpper - min) / (max - min)
-        );
-       
-        /* 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 (actor, which, value) { 
-            this.emit('drag-end', which, this._values[which]);
-        }));
-        // 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.numberLabelLower, {x_align: St.Align.START});
-        this.topBox.add(this.label, {expand: true, x_align: St.Align.MIDDLE});
-        this.topBox.add(this.numberLabelUpper, {x_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 (which, raw) {
-        if (raw) {
-            return this.slider.getValue(which);
-        } else {
-            return this._values[which];
-        }
-    },
-
-    getLowerValue: function (raw) {
-        return this.getValue(0, raw);
-    },
-
-    getUpperValue: function (raw) {
-        return this.getValue(1, raw);
-    },
-
-    setLowerValue: function (value, raw) {
-        this.setValue(0, value, raw);
-    },
-
-    setUpperValue: function (value, raw) {
-        this.setValue(1, value, raw);
-    },
-
-    /* sets the value of the slider, either the raw (0-1) value or the
-     * value on the min->max scale */
-    setValue: function (which, value, raw) {
-        value = (raw ? value : (value - this.min) / (this.max - this.min));
-        this._updateValue(this.slider, which, value);
-        this.slider.setValue(which, value);
-    },
-
-    _updateValue: function (slider, which, value) {
-        let val = value * (this.max - this.min) + this.min;
-        if (this.round) {
-            val = Math.round(val);
-        }
-        this._values[which] = val;
-        if (which === 0) {
-            this.numberLabelLower.set_text(val.toFixed(this.ndec));
-        } else {
-            this.numberLabelUpper.set_text(val.toFixed(this.ndec));
-        }
-    }
-};
-/* 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 DoubleSliderPopupMenuItem() {
-    this._init.apply(this, arguments);
-}
-DoubleSliderPopupMenuItem.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 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 TypeError('The slider value must be a number');
-
-        this._value[i] = Math.max(Math.min(value, 1), 0);
-        this._slider.queue_repaint();
-    },
-
-    getValue: function (which) {
-        return this._values[which];
-    },
-
-    _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(actor, event, which) {
-        if (this._dragging) {
-            this._slider.disconnect(this._releaseId);
-            this._slider.disconnect(this._motionId);
-
-            Clutter.ungrab_pointer();
-            this._dragging = false;
-
-            this.emit('drag-end', which, this._values[which]);
-        }
-        return true;
-    },
-
-
-    _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);
-        // DOT
-        this._releaseId = this._slider.connect('button-release-event', Lang.bind(this, this._endDragging, dot));
-        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 == 0 ? 0 : this._values[0], 
-            Math.min(newvalue, which == 0 ? this._values[1] : 1));
-        this._values[which] = newvalue;
-        this._slider.queue_repaint();
-        this.emit('value-changed', which, this._values[which]);
-    }
-};
-
-/* A slider with a label + number that updates with the slider
- * 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 SliderMenuItem() {
-    this._init.apply(this, arguments);
-}
-SliderMenuItem.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;
-        }
-        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));
-    },
-};
-
-function ThemeSliderMenuItem() {
-    this._init.apply(this, arguments);
-}
-
-ThemeSliderMenuItem.prototype = {
-    __proto__: SliderMenuItem.prototype,
-
-    _init: function () {
-        SliderMenuItem.prototype._init.apply(this, arguments);
-
-        /* Icon (default no icon) */
-        this.icon = new St.Icon({
-            icon_name: 'image-missing', // placeholder icon
-            icon_type: St.IconType.FULLCOLOR,
-            style_class: 'popup-menu-icon'
-        });
-
-        /* Info button */
-        this.button = new St.Button();
-        let icon = new St.Icon({
-            icon_name: 'help-contents',
-            style_class: 'popup-menu-icon',
-            icon_type: St.IconType.FULLCOLOR
-        });
-        this.button.set_child(icon);
-
-        this.label.add_style_class_name('theme-slider-menu-item-label');
-        // Note: if I set the padding in the css it gets overridden
-        this.slider.actor.set_style('padding-left: 0.5em; padding-right: 0em;');
-
-        /* connect up signals */
-        this.button.connect('clicked', Lang.bind(this, function () {
-            this.emit('button-clicked');
-        }));
-
-        /* assemble the item */
-        // polyglot insert_before/insert_child_at_index
-        if (this.topBox.insert_before) {
-            this.topBox.insert_before(this.icon, this.label);
-            this.bottomBox.insert_before(this.button, this.slider.actor);
-        } else {
-            this.topBox.insert_child_at_index(this.icon, 0);
-            this.bottomBox.insert_child_at_index(this.button, 0);
-        }
-    },
-
-    /* sets the icon from a path */
-    setIcon: function () {
-        AboutDialog.prototype.setIcon.apply(this, arguments);
-    }
-};
-
-function LoadAverageSliderMenuItem() {
-    this._init.apply(this, arguments);
-}
-
-LoadAverageSliderMenuItem.prototype = {
-    __proto__: DoubleSliderMenuItem.prototype,
-
-    _init: function () {
-        DoubleSliderMenuItem.prototype._init.apply(this, arguments);
-
-        /* set styles */
-        this.numberLabelLower.add_style_class_name('xpenguins-load-averaging');
-        this.numberLabelUpper.add_style_class_name('xpenguins-load-averaging');
-    },
-
-    setBeingUsed: function(usedLower, usedUpper) {
-        if (usedLower) {
-            this.numberLabelLower.add_style_pseudo_class('loadAveragingActive');
-        } else {
-            this.numberLabelLower.remove_style_pseudo_class('loadAveragingActive');
-        }
-        if (usedUpper) {
-            this.numberLabelUpper.add_style_pseudo_class('loadAveragingActive');
-        } else {
-            this.numberLabelUpper.remove_style_pseudo_class('loadAveragingActive');
-        }
-    }
-}
-
 /*
  * XPenguinsMenu Object
  */
         }
 
         /* animation speed */
-        this._items.delay = new SliderMenuItem(_("Time between frames (ms)"),
+        this._items.delay = new UI.SliderMenuItem(_("Time between frames (ms)"),
                 60, 10, 200, true);
         this._optionsMenu.menu.addMenuItem(this._items.delay);
         this._items.delay.connect('drag-end', Lang.bind(this, this.changeOption,
         /* Load averaging. */
         // TODO: what is reasonable? look at # CPUs and times by fudge factor?
         if (!blacklist.loadAveraging) {
-            this._items.loadAveraging = new LoadAverageSliderMenuItem(_("Load average reduce threshold"),
+            this._items.loadAveraging = new UI.LoadAverageSliderMenuItem(_("Load average reduce threshold"),
                     -0.01, 2, -0.01, 2, false, 2);
             this._optionsMenu.menu.addMenuItem(this._items.loadAveraging);
             this._items.loadAveraging.connect('drag-end', Lang.bind(this, function (slider, which, val) {
             this._themeInfo = ThemeManager.describeThemes(themeList, false);
             for (let i = 0; i < themeList.length; ++i) {
                 let sanitised_name = ThemeManager.sanitiseThemeName(themeList[i]);
-                this._items.themes[sanitised_name] = new ThemeSliderMenuItem(
+                this._items.themes[sanitised_name] = new UI.ThemeSliderMenuItem(
                     _(themeList[i]), 0, 0, XPenguins.PENGUIN_MAX, true);
                 this._items.themes[sanitised_name].setIcon(this._themeInfo[sanitised_name].icon);
                 this._items.themes[sanitised_name].connect('drag-end',
             this._themeInfo[name] = ThemeManager.describeThemes([name], false)[name];
         }
 
-        let dialog = new AboutDialog(this._themeInfo[name].name);
+        let dialog = new UI.AboutDialog(this._themeInfo[name].name);
         for (let i = 0; i < this._ABOUT_ORDER.length; ++i) {
             let propName = this._ABOUT_ORDER[i];
             if (this._themeInfo[name][propName]) {
 
     _onChooseWindow: function () {
         XPUtil.DEBUG('[ext] _onChooseWindow');
-        let dialog = new WindowPickerDialog();
+        let dialog = new UI.WindowPickerDialog();
         dialog.open(global.get_current_time());
         dialog._windowSelectedID = dialog.connect('window-selected', Lang.bind(this, this._onWindowChosen));
     },

xpenguins@mathematical.coffee.gmail.com/metadata.json

 {
  "extension-id": "xpenguins",
  "uuid": "xpenguins@mathematical.coffee.gmail.com",
+ "settings-schema": "org.gnome.shell.extensions.xpenguins",
+ "gettext-domain": "xpenguins",
  "name": "xpenguins",
- "description": "A port of XPenguins to gnome-shell. See extension home page for explanation of configuration options.",
+ "description": "A port of XPenguins to gnome-shell! See extension home page for explanation of configuration options.",
  "shell-version": [ 
      "3.2"
  ],
  "url": "https://bitbucket.org/mathematicalcoffee/xpenguins-gnome-shell-extension",
- "dev-version": "2.0"
+ "version": 1,
+ "dev-version": "2.0_gnome3.2"
 }

xpenguins@mathematical.coffee.gmail.com/theme.js

 const XPenguins = Me.xpenguins;
 const XPUtil = Me.util;
 
+const Gettext = imports.gettext.domain('xpenguins');
+const _ = Gettext.gettext;
+
 /***********************
  *    Theme Object     *
  ***********************/
 
 Theme.prototype = {
     _init: function (themeList) {
-        XPUtil.DEBUG('creating theme');
+        XPUtil.LOG('creating theme');
         /* members */
         /* Theme: can have one or more genera
          * Genus: class of toons (Penguins has 2: walker & skateboarder).
         }
         words = words.trim().split(' ');
 
+
+        /* make space for the next toon */
         try {
             for (let i = 0; i < words.length; ++i) {
                 let word = words[i].toLowerCase();

xpenguins@mathematical.coffee.gmail.com/toon.js

 const Clutter = imports.gi.Clutter;
 const Lang = imports.lang;
 
-const Gettext = imports.gettext.domain('gnome-shell-extensions');
+const Gettext = imports.gettext.domain('xpenguins');
 const _ = Gettext.gettext;
 
 const Me = imports.ui.extensionSystem.extensions['xpenguins@mathematical.coffee.gmail.com'];

xpenguins@mathematical.coffee.gmail.com/ui.js

+const Clutter  = imports.gi.Clutter;
+const Gio      = imports.gi.Gio;
+const Gtk      = imports.gi.Gtk;
+const Lang     = imports.lang;
+const Meta     = imports.gi.Meta;
+const Pango    = imports.gi.Pango;
+const St       = imports.gi.St;
+
+const AltTab    = imports.ui.altTab;
+const ModalDialog = imports.ui.modalDialog;
+const PopupMenu = imports.ui.popupMenu;
+
+const Gettext = imports.gettext.domain('xpenguins');
+const _ = Gettext.gettext;
+
+/*
+ * Various UI elements.
+ */
+
+function WindowPickerDialog() {
+    this._init.apply(this, arguments);
+}
+
+WindowPickerDialog.prototype = {
+    __proto__: ModalDialog.ModalDialog.prototype,
+
+    _init: function () {
+        ModalDialog.ModalDialog.prototype._init.call(this,
+            {styleClass: 'modal-dialog'});
+
+        let monitor = global.screen.get_monitor_geometry(global.screen.get_primary_monitor()),
+            width   = Math.round(monitor.width * .6),
+            height  = Math.min(300, Math.round(monitor.height * .3));
+
+        /* title + icon */
+        let box = new St.BoxLayout();
+        box.add(new St.Label({text: _("Select which window to run XPenguins in, or 'Cancel' to use the Desktop:")}),
+                    {x_fill: true});
+        this.contentLayout.add(box, {x_fill: true});
+
+        /* scroll box */
+        this.scrollBox = new St.ScrollView({
+            x_fill: true,
+            y_fill: true,
+            width: width,
+            height: height
+        });
+        // automatic horizontal scrolling, no vertical scrolling
+        this.scrollBox.set_policy(Gtk.PolicyType.AUTOMATIC,
+            Gtk.PolicyType.NEVER);
+
+        /* thumbnails in scroll box (put in BoxLayout or else cannot see) */
+        let box = new St.BoxLayout();
+        this._windows = global.get_window_actors().map(function (w) {
+            return w.meta_window;
+        });
+        /* filter out Nautilus desktop window */
+        this._windows = this._windows.filter(function (w) {
+            return w.window_type != Meta.WindowType.DESKTOP;
+        });
+        this._thumbnails = new AltTab.ThumbnailList(this._windows);
+        this._thumbnails.actor.get_allocation_box();
+        box.add(this._thumbnails.actor, {expand: true, x_fill: true, y_fill: true});
+        this.scrollBox.add_actor(box,
+            {expand: true, x_fill: true, y_fill: true});
+        this.contentLayout.add(this.scrollBox, {expand: true, x_fill: true, y_fill: true});
+        // need to call addClones at some point. it was called in _allocate ...
+
+        /* Cancel button */
+        this.setButtons([{
+            label: _("Cancel"),
+            action: Lang.bind(this, function () {
+                this._windowActivated(this._thumbnails, -1);
+            })
+        }]);
+	},
+
+    open: function() {
+        ModalDialog.ModalDialog.prototype.open.apply(this, arguments);
+        this._thumbnails.addClones(this.scrollBox.height);
+        this._thumbnails.connect('item-activated', Lang.bind(this, this._windowActivated));
+        this._thumbnails.connect('item-entered', Lang.bind(this, this._windowEntered));
+    },
+
+    _windowActivated: function (thumbnails, n) {
+        this.emit('window-selected', this._windows[n]);
+        this.close(global.get_current_time());
+    },
+
+    _windowEntered: function (thumbnails, n) {
+        this._thumbnails.highlight(n);
+    }
+};
+
+/* Popup dialog with scrollable text.
+ * See InstallExtensionDialog in extensionSystem.js for an example.
+ *
+ * Future icing: make one toon of each type in the theme and have them run
+ * in the about dialog.
+ */
+function AboutDialog() {
+    this._init.apply(this, arguments);
+}
+
+AboutDialog.prototype = {
+    __proto__: ModalDialog.ModalDialog.prototype,
+
+    _init: function (title, text, icon_path) {
+        ModalDialog.ModalDialog.prototype._init.call(this,
+            {styleClass: 'modal-dialog'});
+
+        let monitor = global.screen.get_monitor_geometry(global.screen.get_primary_monitor()),
+            width   = Math.max(400, Math.round(monitor.width / 3)),
+            height  = Math.max(400, Math.round(monitor.height / 2.5));
+
+        /* title + icon */
+        this.titleBox = new St.BoxLayout({vertical: false});
+        this.contentLayout.add(this.titleBox,
+            {x_fill: false, x_align: St.Align.MIDDLE});
+
+        this.icon = new St.Icon({
+            icon_name: 'image-missing',
+            icon_type: St.IconType.FULLCOLOR,
+            style_class: 'xpenguins-about-icon'
+        });
+        this.setIcon(icon_path);
+        this.titleBox.add(this.icon);
+
+        this.title = new St.Label({text: title || '',
+            style_class: 'xpenguins-about-title'});
+        this.titleBox.add(this.title,  {x_fill: true});
+
+        /* scroll box */
+        this.scrollBox = new St.ScrollView({
+            x_fill: true,
+            y_fill: true,
+            width: width,
+            height: height
+        });
+        // automatic horizontal scrolling, automatic vertical scrolling
+        this.scrollBox.set_policy(Gtk.PolicyType.AUTOMATIC,
+            Gtk.PolicyType.AUTOMATIC);
+
+        /* text in scrollbox.
+         * For some reason it won't display unless in a St.BoxLayout. */
+        this.text = new St.Label({text: (text || ''),
+            style_class: 'xpenguins-about-text'});
+        this.text.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; // allows scrolling
+        //this.text.clutter_text.line_wrap = true;
+
+        this.box = new St.BoxLayout();
+        this.box.add(this.text, { expand: true });
+        this.scrollBox.add_actor(this.box,
+            {expand: true, x_fill: true, y_fill: true});
+        this.contentLayout.add(this.scrollBox,
+            {expand: true, x_fill: true, y_fill: true});
+
+        /* OK button */
+        this.setButtons([{
+            label: _("OK"),
+            action: Lang.bind(this, function () {
+                this.close(global.get_current_time());
+            })
+        }]);
+	},
+
+    setTitle: function (title) {
+        this.title.text = title;
+    },
+
+    setText: function (text) {
+        this.text.text = text;
+    },
+
+    appendText: function (text, sep) {
+        this.text.text += (sep || '\n') + text;
+    },
+
+    setIcon: function (icon_path) {
+        let path = icon_path ? Gio.file_new_for_path(icon_path) : null;
+        if (path && path.query_exists(null)) {
+            this.icon.set_gicon(new Gio.FileIcon({file: path}));
+        }
+    }
+};
+
+/* A DoubleSliderPopupMenuItem paired with a text label & two number labels */
+function DoubleSliderMenuItem() {
+    this._init.apply(this, arguments);
+}
+
+DoubleSliderMenuItem.prototype = {
+    __proto__: PopupMenu.PopupBaseMenuItem.prototype,
+
+    _init: function (text, valLower, valUpper, 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._values = [valLower, valUpper];
+        this._numVals = this._values.length; // pre-cache
+        if (round) {
+            this._values = this._values.map(function (v) {
+                return Math.round(v);
+            });
+        }
+        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: 'double-slider-menu-item-top-box'});
+        this.box.add(this.topBox, {x_fill: true});
+
+        this.bottomBox = new St.BoxLayout({vertical: false, 
+            style_class: 'double-slider-menu-item-bottom-box'});
+        this.box.add(this.bottomBox, {x_fill: true});
+
+        /* text */
+        this.label = new St.Label({text: text, reactive: false,
+            style_class: 'double-slider-menu-item-label'});
+
+        /* numbers */
+        this.numberLabelLower = new St.Label({text: this._values[0].toFixed(this.ndec), 
+            reactive: false});
+        this.numberLabelUpper = new St.Label({text: this._values[1].toFixed(this.ndec), 
+            reactive: false});
+        this.numberLabelLower.add_style_class_name('double-slider-menu-item-number-label');
+        this.numberLabelUpper.add_style_class_name('double-slider-menu-item-number-label');
+
+        /* slider */
+        this.slider = new DoubleSliderPopupMenuItem(
+            (valLower - min) / (max - min),
+            (valUpper - min) / (max - min)
+        );
+       
+        /* 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 (actor, which, value) { 
+            this.emit('drag-end', which, this._values[which]);
+        }));
+        // 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.numberLabelLower, {x_align: St.Align.START});
+        this.topBox.add(this.label, {expand: true, x_align: St.Align.MIDDLE});
+        this.topBox.add(this.numberLabelUpper, {x_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 (which, raw) {
+        if (raw) {
+            return this.slider.getValue(which);
+        } else {
+            return this._values[which];
+        }
+    },
+
+    getLowerValue: function (raw) {
+        return this.getValue(0, raw);
+    },
+
+    getUpperValue: function (raw) {
+        return this.getValue(1, raw);
+    },
+
+    setLowerValue: function (value, raw) {
+        this.setValue(0, value, raw);
+    },
+
+    setUpperValue: function (value, raw) {
+        this.setValue(1, value, raw);
+    },
+
+    /* sets the value of the slider, either the raw (0-1) value or the
+     * value on the min->max scale */
+    setValue: function (which, value, raw) {
+        value = (raw ? value : (value - this.min) / (this.max - this.min));
+        this._updateValue(this.slider, which, value);
+        this.slider.setValue(which, value);
+    },
+
+    _updateValue: function (slider, which, value) {
+        let val = value * (this.max - this.min) + this.min;
+        if (this.round) {
+            val = Math.round(val);
+        }
+        this._values[which] = val;
+        if (which === 0) {
+            this.numberLabelLower.set_text(val.toFixed(this.ndec));
+        } else {
+            this.numberLabelUpper.set_text(val.toFixed(this.ndec));
+        }
+    }
+};
+/* 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 DoubleSliderPopupMenuItem() {
+    this._init.apply(this, arguments);
+}
+DoubleSliderPopupMenuItem.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 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 TypeError('The slider value must be a number');
+
+        this._value[i] = Math.max(Math.min(value, 1), 0);
+        this._slider.queue_repaint();
+    },
+
+    getValue: function (which) {
+        return this._values[which];
+    },
+
+    _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(actor, event, which) {
+        if (this._dragging) {
+            this._slider.disconnect(this._releaseId);
+            this._slider.disconnect(this._motionId);
+
+            Clutter.ungrab_pointer();
+            this._dragging = false;
+
+            this.emit('drag-end', which, this._values[which]);
+        }
+        return true;
+    },
+
+
+    _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);
+        // DOT
+        this._releaseId = this._slider.connect('button-release-event', Lang.bind(this, this._endDragging, dot));
+        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 == 0 ? 0 : this._values[0], 
+            Math.min(newvalue, which == 0 ? this._values[1] : 1));
+        this._values[which] = newvalue;
+        this._slider.queue_repaint();
+        this.emit('value-changed', which, this._values[which]);
+    }
+};
+
+/* A slider with a label + number that updates with the slider
+ * 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 SliderMenuItem() {
+    this._init.apply(this, arguments);
+}
+SliderMenuItem.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});
+
+        /* Debugging */
+        /*
+        this.box.set_style('border: 1px solid red;');
+        this.topBox.set_style('border: 1px solid green;');
+        this.bottomBox.set_style('border: 1px solid blue;');
+        */
+    },
+
+    /* 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;
+        }
+        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));
+    },
+};
+
+function ThemeSliderMenuItem() {
+    this._init.apply(this, arguments);
+}
+
+ThemeSliderMenuItem.prototype = {
+    __proto__: SliderMenuItem.prototype,
+
+    _init: function () {
+        SliderMenuItem.prototype._init.apply(this, arguments);
+
+        /* Icon (default no icon) */
+        this.icon = new St.Icon({
+            icon_name: 'image-missing', // placeholder icon
+            icon_type: St.IconType.FULLCOLOR,
+            style_class: 'popup-menu-icon'
+        });
+
+        /* Info button */
+        this.button = new St.Button();
+        let icon = new St.Icon({
+            icon_name: 'help-contents',
+            style_class: 'popup-menu-icon',
+            icon_type: St.IconType.FULLCOLOR
+        });
+        this.button.set_child(icon);
+
+        this.label.add_style_class_name('theme-slider-menu-item-label');
+        // Note: if I set the padding in the css it gets overridden
+        this.slider.actor.set_style('padding-left: 0.5em; padding-right: 0em;');
+
+        /* connect up signals */
+        this.button.connect('clicked', Lang.bind(this, function () {
+            this.emit('button-clicked');
+        }));
+
+        /* assemble the item */
+        // polyglot insert_before/insert_child_at_index
+        if (this.topBox.insert_before) {
+            this.topBox.insert_before(this.icon, this.label);
+            this.bottomBox.insert_before(this.button, this.slider.actor);
+        } else {
+            this.topBox.insert_child_at_index(this.icon, 0);
+            this.bottomBox.insert_child_at_index(this.button, 0);
+        }
+    },
+
+    /* sets the icon from a path */
+    setIcon: function () {
+        AboutDialog.prototype.setIcon.apply(this, arguments);
+    }
+};
+
+function LoadAverageSliderMenuItem() {
+    this._init.apply(this, arguments);
+}
+
+LoadAverageSliderMenuItem.prototype = {
+    __proto__: DoubleSliderMenuItem.prototype,
+
+    _init: function () {
+        DoubleSliderMenuItem.prototype._init.apply(this, arguments);
+
+        /* set styles */
+        this.numberLabelLower.add_style_class_name('xpenguins-load-averaging');
+        this.numberLabelUpper.add_style_class_name('xpenguins-load-averaging');
+    },
+
+    setBeingUsed: function(usedLower, usedUpper) {
+        if (usedLower) {
+            this.numberLabelLower.add_style_pseudo_class('loadAveragingActive');
+        } else {
+            this.numberLabelLower.remove_style_pseudo_class('loadAveragingActive');
+        }
+        if (usedUpper) {
+            this.numberLabelUpper.add_style_pseudo_class('loadAveragingActive');
+        } else {
+            this.numberLabelUpper.remove_style_pseudo_class('loadAveragingActive');
+        }
+    }
+}
+

xpenguins@mathematical.coffee.gmail.com/util.js

         [].shift.call(arguments);
         msg = ''.format.apply(msg, arguments);
     }
-    log(msg);
+    //log(msg);
     return msg;
 }
 
 /* utility warning function */
 function warn() {
     let msg = LOG.apply(null, arguments);
+    log(msg);
     global.log(msg);
 }
 
 function error() {
     let msg = LOG.apply(null, arguments);
+    log(msg);
     global.log(msg);
     throw new Error(msg);
 }

xpenguins@mathematical.coffee.gmail.com/windowListener.js

             if (i_options.hasOwnProperty(opt) && this.options.hasOwnProperty(opt)) {
                 this.options[opt] = i_options[opt];
             } else {
-                LOG('  option %s not supported yet', opt);
+                this.LOG('  option %s not supported yet', opt);
             }
         }
 

xpenguins@mathematical.coffee.gmail.com/xpenguins.js

 
 const Main = imports.ui.main;
 
-const Gettext = imports.gettext.domain('gnome-shell-extensions');
+const Gettext = imports.gettext.domain('xpenguins');
 const _ = Gettext.gettext;
 
 const Me = imports.ui.extensionSystem.extensions['xpenguins@mathematical.coffee.gmail.com'];