Commits

mathematicalcoffee committed 159651c Merge

GNOME 3.2 v1.0 - merged the theme sliders features that got merged into gnome3.4, down to gnome3.2 too.

  • Participants
  • Parent commits 8aba5e2, 3950eab
  • Branches gnome3.2
  • Tags v1.0_gnome3.2

Comments (0)

Files changed (14)

+1691e873be15a1ad0c3627db5f17615032db9f8e branch-point
+1691e873be15a1ad0c3627db5f17615032db9f8e branch-point
+c679ed1646d47dd45a7556a4e51110ade3fefd65 branch-point
+602e0238e9cec80fa3385b233af3065a3f254922 v1.0_gnome3.4
+#=============================================================================
+EXTENSION=xpenguins
+EXTENSION_BASE=@mathematical.coffee.gmail.com
+FILES=metadata.json *.js stylesheet.css penguin.png themes
+#=============================================================================
+default_target: all
+.PHONY: clean all zip
+
+clean:
+	rm -f $(EXTENSION)$(EXTENSION_BASE).zip
+
+# nothing in this target, just make the zip
+all:
+
+zip: clean all
+	zip -rq $(EXTENSION)$(EXTENSION_BASE).zip $(FILES:%=$(EXTENSION)$(EXTENSION_BASE)/%)
 If you have Linux but not GNOME-shell, just use the original XPenguins.
 Even if you have GNOME-shell you can run the original XPenguins, but you have to set Nautilus to handle the desktop, and the toons will think that the windows are bigger than they actually are so it can look weird.
 
-![Screenshot of XPenguins extension](http://cdn.bitbucket.org/mathematicalcoffee/xpenguins-gnome-shell-extension/downloads/xpenguins-screenshot.png)
+| Penguins walk all over your windows | Configure via menu |
+|:-----:|:-----:|
+| ![Screenshot of XPenguins extension](http://cdn.bitbucket.org/mathematicalcoffee/xpenguins-gnome-shell-extension/downloads/xpenguins-screenshot.png) | ![Configure XPenguins](http://cdn.bitbucket.org/mathematicalcoffee/xpenguins-gnome-shell-extension/downloads/xpenguins-menu.png) |
 
 Extension written 2012 by mathematical.coffee [mathematical.coffee@gmail.com](mailto:mathematical.coffee@gmail.com?subject=xpenguins%20question).  
 The original XPenguins was written by Robin Hogan ([http://xpenguins.seul.org/](http://xpenguins.seul.org/)).  
 # Known issues
 Here are some known issues/limitations of the program (if you think you can fix one, feel free to check out the code and have a go!)
 
+* 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.
 
 # Wish list

File xpenguins-menu.png

Added
New image

File xpenguins@mathematical.coffee.gmail.com/TODO

 --------------------------
            UPTO
 --------------------------
-* jslint again.
 * error handling: what to do on throw new Error
 * [bookmark] load averaging + options/sliders.
-* edge block sidebottomblock
+* test edge block sidebottomblock
 
 * sometimes: fatal IO error 11 Resource temporarily unavailable.
 
 - [wont] remove windowListener?
 
 BRANCH GNOME 3.4  [from 3.2]
-- misc.extensionutils
+- [yep] misc.extensionutils
+- [yep] remove windowListener from extension.js
+- [wont] remove windowListener?
 - toon == actor (??)
-- remove windowListener from extension.js
-- remove windowListener?
 Toon.Toon = new Lang.Class({
     Name: 'Toon',
     Extends: Clutter.Clone,
     _init: ....
 });
+
 BRANCH default
 - polyglot (try/catch for diff versions)
 

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

+/* Notes:
+ * - fps slider to speed them up?
+ * - load averaging sliders
+ */
+
+/* *** CODE *** */
 const Gio      = imports.gi.Gio;
 const GLib     = imports.gi.GLib;
 const Gtk      = imports.gi.Gtk;
 
 /* my files */
 const Me = imports.ui.extensionSystem.extensions['xpenguins@mathematical.coffee.gmail.com'];
-const ThemeManager = Me.themeManager.ThemeManager;
+const ThemeManager = Me.themeManager;
 const WindowListener = Me.windowListener;
 const XPenguins = Me.xpenguins;
 const XPUtil = Me.util;
 
 /* Popup dialog with scrollable text.
  * See InstallExtensionDialog in extensionSystem.js for an example.
- * FIXME:  styles for title, icon, ...
+ *
+ * Future icing: make on 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) {
-        ModalDialog.ModalDialog.prototype._init.call(this, {styleClass: 'modal-dialog'});
+    _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(250, Math.round(monitor.width / 4)),
             height  = Math.max(400, Math.round(monitor.height / 2.5));
 
-        /* title */
-        this.title = new St.Label({text: title || '', style_class: 'xpenguins-about-title'});
-        this.contentLayout.add(this.title, {x_fill: false, x_align: St.Align.MIDDLE});
+        /* 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({
             height: height
         });
         // automatic horizontal scrolling, automatic vertical scrolling
-        this.scrollBox.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+        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'});
+        /* 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});
+        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()); })
+            action: Lang.bind(this, function () {
+                this.close(global.get_current_time()); 
+            })
         }]);
 	},
 
 
     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}));
+        }
     }
 };
 
-
-function ThemeMenuItem() {
+function ThemeSliderMenuItem() {
     this._init.apply(this, arguments);
 }
 
-ThemeMenuItem.prototype = {
+ThemeSliderMenuItem.prototype = {
     __proto__: PopupMenu.PopupBaseMenuItem.prototype,
 
-    _init: function (text, state, icon_path, params) {
+    _init: function (text, defaultVal, min, max, round, icon_path, params) {
         PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);
-        /* NOTE: if I just use this.addActor there's heaps of space between all the items,
-         * regardless of setting this.actor's spacing or padding to 0, same with constituent items.
-         * So currently using this.box  and this.box.add.
-         */
-        this.actor.set_style('padding-top: 0px; padding-bottom: 0px');
-        this.box = new St.BoxLayout({vertical: false});
+
+        /* 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);
+        } 
+
+        /* set up item */
+        this.box = new St.BoxLayout({vertical: true, name: 'xpenguins'});
         this.addActor(this.box, {expand: true, span: -1});
 
+        this.topBox = new St.BoxLayout({vertical: false, 
+            style_class: 'theme-slider-menu-item-top-box'});
+        this.topBox.add_style_class_name('theme-slider-menu-item-top-box');
+        this.box.add(this.topBox, {x_fill: true});
+
+        this.bottomBox = new St.BoxLayout({vertical: false, 
+            style_class: 'theme-slider-menu-item-bottom-box'});
+        this.box.add(this.bottomBox, {x_fill: true});
+
+        /* 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'
+        });
+        this.setIcon(icon_path);
+
+        /* text */
+        this.label = new St.Label({text: text, reactive: false});
+        this.label.set_style('padding-left: 0.5em');
+
+        /* number */
+        this.numberLabel = new St.Label({text: this._value.toString(), 
+            reactive: false});
+
         /* Info button */
-        /* Could just set style-class with background-image... */
         this.button = new St.Button();
         let icon = new St.Icon({
             icon_name: 'help-contents',
             icon_type: St.IconType.FULLCOLOR
         });
         this.button.set_child(icon);
-        this.box.add(this.button);
 
-        /* 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'
-        });
-        this.box.add(this.icon);
-        this.setIcon(icon_path);
+        /* slider */
+        this.slider = new PopupMenu.PopupSliderMenuItem((defaultVal - min) / 
+            (max - min)); // between 0 and 1
+        this.slider.actor.set_style('padding-left: 0.5em; padding-right: 0em');
+       
+        /* 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); 
+        }));
+        this.button.connect('clicked', Lang.bind(this, function () { 
+            this.emit('button-clicked'); 
+        }));
 
-        /* toggle. */
-        this.toggle = new PopupMenu.PopupSwitchMenuItem(text, state || false);
-        this.box.add(this.toggle.actor, {expand: true, align: St.Align.END});
+        /* assemble the item */
+        this.topBox.add(this.icon);
+        this.topBox.add(this.label, {expand: true});
+        this.topBox.add(this.numberLabel, {align: St.Align.END});
+        this.bottomBox.add(this.button);
+        this.bottomBox.add(this.slider.actor, {expand: true, span: -1});
+    },
 
-        /* Pass through events */
-        this.toggle.connect('toggled', Lang.bind(this, function () { this.emit('toggled', this.toggle.state); }));
-        this.button.connect('clicked', Lang.bind(this, function () { this.emit('button-clicked'); }));
+    /* hope that this.slider.value and this._value remain in sync... */
+    getValue: function (raw) {
+        if (raw) {
+            return this.slider.value;
+        } else {
+            return this._value;
+        }
+    },
 
+    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.toString());
     },
 
     get state() { return this.toggle.state; },
 
     /* sets the icon from a path */
-    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}));
-        }
+    setIcon: function () {
+        AboutDialog.prototype.setIcon.apply(this, arguments);
     }
 };
 
     _init: function (extensionPath) {
         XPUtil.DEBUG('_init');
         /* Initialise */
-        PanelMenu.SystemStatusButton.prototype._init.call(this, 'emblem-favorite', 'xpenguins');
+        PanelMenu.SystemStatusButton.prototype._init.call(this, 
+            'emblem-favorite', 'xpenguins');
+        this.actor.add_style_class_name('xpenguins-icon');
         this.setGIcon(new Gio.FileIcon({
-            file: Gio.file_new_for_path(GLib.build_filenamev([extensionPath, 'penguin.png']))
+            file: Gio.file_new_for_path(GLib.build_filenamev([extensionPath, 
+                      'penguin.png']))
         }));
 
         /* items */
         this._items = {};
         this._themeInfo = {};
         this._toggles = {
-            ignorePopups   : _("Ignore popups"),
-            ignoreMaximised: _("Ignore maximised windows"),
-            onAllWorkspaces: _("Always on visible workspace"), // <-- this is the only one
-            onDesktop      : _("Run on desktop"), // not fully implemented
-            blood          : _("Show blood"),
-            angels         : _("Show angels"),
-            squish         : _("God Mode"),
+            ignorePopups       : _("Ignore popups"),
+            ignoreMaximised    : _("Ignore maximised windows"),
+            ignoreHalfMaximised: _(".. and half-maximised too"),
+            onAllWorkspaces    : _("Always on visible workspace"),
+            onDesktop          : _("Run on desktop"), // not fully implemented
+            blood              : _("Show blood"),
+            angels             : _("Show angels"),
+            squish             : _("God Mode"),
         };
         this._ABOUT_ORDER = ['name', 'date', 'artist', 'copyright',
-            'license', 'maintainer', 'location', 'icon', 'comment'];
+            'license', 'maintainer', 'location', 'comment'];
         this._THEME_STRING_LENGTH_MAX = 15;
 
         /* Create menus */
 
         /* create an Xpenguin Loop object which stores the XPenguins program */
         this._XPenguinsLoop = new XPenguins.XPenguinsLoop(this.getConf());
+        /* Listen to 'ntoons-changed' and adjust slider accordingly */
+        this._XPenguinsLoop.connect('ntoons-changed', Lang.bind(this, 
+            this._onChangeThemeNumber));
+
+        /* @@ debugging windowListener */
+        this._windowListener = new WindowListener.WindowListener();
 
         /* initialise as 'Penguins' */
-        this._onChangeTheme(null, true, 'Penguins');
+        /* by default, just Penguins is set */
+        if (this._items.themes['Penguins']) {
+            this._onChangeTheme(this._items.themes['Penguins'], -1, 'Penguins', 
+                false);
+        }
     },
 
     getConf: function () {
         this.menu.removeAll();
 
         /* toggle to start xpenguins */
-        this._items.start = new PopupMenu.PopupSwitchMenuItem(_("Start"), false);
-        this._items.start.connect('toggled', Lang.bind(this, this._startXPenguins));
+        this._items.start = new PopupMenu.PopupSwitchMenuItem(_("Start"), 
+            false);
+        this._items.start.connect('toggled', Lang.bind(this, 
+            this._startXPenguins));
         this.menu.addMenuItem(this._items.start);
 
         /* theme submenu */
         this._optionsMenu = new PopupMenu.PopupSubMenuMenuItem(_("Options"));
         this.menu.addMenuItem(this._optionsMenu);
 
-        /* Number of penguins */
-        dummy = new PopupMenu.PopupMenuItem(_("Max penguins"), { reactive: false });
-        this._items.nPenguinsLabel = new St.Label({ text: '-1' });
-        dummy.addActor(this._items.nPenguinsLabel, { align: St.Align.END });
-        this._optionsMenu.menu.addMenuItem(dummy);
-
-        // set to default from theme that was just loaded.
-        this._items.nPenguins = new PopupMenu.PopupSliderMenuItem(0);
-        this._items.nPenguins.connect('value-changed', Lang.bind(this, this._nPenguinsSliderChanged));
-        this._items.nPenguins.connect('drag-end', Lang.bind(this, this._onNPenguinsChanged));
-        this._optionsMenu.menu.addMenuItem(this._items.nPenguins);
-
-        /* ignore maximised, always on visible workspace, angels, blood, god mode, verbose toggles */
+        /* ignore maximised, ignore popups, ignore half maximised, god mode,
+         * always on visible workspace, angels, blood, verbose toggles */
         let defaults = XPenguins.XPenguinsLoop.prototype.defaultOptions();
         let blacklist = XPenguins.getCompatibleOptions(true);
         for (let propName in this._toggles) {
             if (this._toggles.hasOwnProperty(propName) && !blacklist[propName]) {
-                this._items[propName] = new PopupMenu.PopupSwitchMenuItem(this._toggles[propName], defaults[propName] || false);
-                this._items[propName].connect('toggled', Lang.bind(this, this.changeOption, propName));
+                this._items[propName] = new PopupMenu.PopupSwitchMenuItem(
+                    this._toggles[propName], defaults[propName] || false);
+                this._items[propName].connect('toggled', 
+                    Lang.bind(this, this.changeOption, propName));
                 this._optionsMenu.menu.addMenuItem(this._items[propName]);
             }
         }
 
+        /* ignore half maximised should be greyed out/unusable if
+         * 'ignoreMaximised' is false, and usable if it's true.
+         * reactive: false?
+         */
+        if (this._items.ignoreHalfMaximised && this._items.ignoreMaximised) {
+            this._items.ignoreMaximised.connect('toggled', Lang.bind(this,
+                function (item, state) {
+                    this._items.ignoreHalfMaximised.setSensitive(state);
+                }));
+            this._items.ignoreHalfMaximised.setSensitive(this._items.ignoreMaximised.state);
+            // FIXME: would be nice for the toggle to look disabled too.
+        }
+
         /* RecalcMode combo box: only if global.display has grab-op- events. */
         if (!blacklist.recalcMode) {
-            //dummy = new PopupMenu.PopupMenuItem(_("Recalc mode"), {reactive: false});
-            //this._optionsMenu.menu.addMenuItem(dummy);
             this._items.recalc = new PopupMenu.PopupComboBoxMenuItem({});
             this._optionsMenu.menu.addMenuItem(this._items.recalc);
             for (let mode in XPenguins.RECALC) {
         if (themeList.length === 0) {
             this._themeMenu.label.set_text(_("No themes found, click to reload!"));
             // FIXME: test
-            this._themeMenu.connect('open', Lang.bind(this, this._populateThemeMenu));
+            this._themeMenu.connect('open', 
+                Lang.bind(this, this._populateThemeMenu));
         } else {
             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 ThemeMenuItem(_(themeList[i]), themeList[i] === 'Penguins');
-                if (this._themeInfo[sanitised_name].icon) {
-                    this._items.themes[sanitised_name].setIcon(this._themeInfo[sanitised_name].icon);
-                }
-                this._items.themes[sanitised_name].connect('toggled', Lang.bind(this, this._onChangeTheme, sanitised_name));
-                this._items.themes[sanitised_name].connect('button-clicked', Lang.bind(this, this._onShowHelp, sanitised_name));
+                this._items.themes[sanitised_name] = new ThemeSliderMenuItem(
+                    _(themeList[i]), 0, 0, XPenguins.PENGUIN_MAX, true,
+                    this._themeInfo[sanitised_name].icon);
+                this._items.themes[sanitised_name].connect('drag-end', 
+                    Lang.bind(this, this._onChangeTheme, sanitised_name, true));
+                this._items.themes[sanitised_name].connect('button-clicked', 
+                    Lang.bind(this, this._onShowHelp, sanitised_name));
                 this._themeMenu.menu.addMenuItem(this._items.themes[sanitised_name]);
             }
         }
                 ));
             }
         }
+        dialog.setIcon(this._themeInfo[name].icon);
         dialog.open(global.get_current_time());
     },
 
-    _onChangeTheme: function (item, state, sanitised_name) {
-        XPUtil.DEBUG('_onChangeTheme');
+    _onChangeTheme: function (item, value, sanitised_name, silent) {
+        XPUtil.DEBUG('_onChangeTheme: ' + sanitised_name);
 
-        let themeList = [];
-        for (let name in this._items.themes) {
-            if (this._items.themes.hasOwnProperty(name) && this._items.themes[name].state) {
-                themeList.push(name);
+        /* set the numbers so we can read them back to the slider bars */
+        this._XPenguinsLoop.setThemeNumbers(sanitised_name, value, silent);
+
+        let themeListFlat = this._XPenguinsLoop.getThemes();
+        if (themeListFlat.length) {
+            themeListFlat = themeListFlat.map(
+                function (name) {
+                    return ThemeManager.prettyThemeName(name);
+                }).reduce(function (x, y) {
+                    return x + ',' + y;
+                });
+            if (themeListFlat.length > this._THEME_STRING_LENGTH_MAX) {
+                themeListFlat = themeListFlat.substr(0, 
+                    this._THEME_STRING_LENGTH_MAX-3) + '...';
             }
-        }
-
-        this._XPenguinsLoop.setThemes(themeList, true);
-
-        let themeListFlat = themeList.map(function (name) {
-                return _(name.replace(/ /g, '_'));
-            }).reduce(function (x, y) {
-                return x + ',' + y;
-            });
-        if (themeListFlat.length > this._THEME_STRING_LENGTH_MAX) {
-            themeListFlat = themeListFlat.substr(0, this._THEME_STRING_LENGTH_MAX-3) + '...';
+        } else {
+            themeListFlat = 'none';
         }
         this._themeMenu.label.set_text(_("Theme") + ' (%s)'.format(themeListFlat));
+    },
 
-        /* Set the label to match */
-        this._items.nPenguins.setValue(this._XPenguinsLoop.options.nPenguins / XPenguins.PENGUIN_MAX);
-        this._items.nPenguinsLabel.set_text(this._XPenguinsLoop.options.nPenguins.toString());
+    _onChangeThemeNumber: function (loop, sanitised_name, n) {
+        XPUtil.DEBUG('[ext] _onChangeThemeNumber[%s] to %d; updating slider',
+            sanitised_name, n);
+        if (n != this._items.themes[sanitised_name].getValue()) {
+            this._items.themes[sanitised_name].setValue(n);
+        }
     },
 
     _startXPenguins: function (item, state) {
         } else {
             this._XPenguinsLoop.stop();
         }
-    },
-
-    _nPenguinsSliderChanged: function (slider, value) {
-        this._items.nPenguinsLabel.set_text(Math.ceil(value * XPenguins.PENGUIN_MAX).toString());
-    },
-
-    _onNPenguinsChanged: function () {
-        if (this._XPenguinsLoop) {
-            this._XPenguinsLoop.setNumber(parseInt(this._items.nPenguinsLabel.get_text(), 10));
-        }
     }
-
 };
 

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

  "uuid": "xpenguins@mathematical.coffee.gmail.com",
  "settings-schema": "org.gnome.shell.extensions.xpenguins",
  "name": "xpenguins",
- "description": "A port of XPenguins to gnome-shell (NOTE: normal xpenguins works anyway, just make sure Nautilus is managing the desktop window).",
+ "description": "A port of XPenguins to gnome-shell. See extension home page for explanation of configuration options",
  "shell-version": [ 
      "3.2" ,
      "3.2.2.1",
      "3.4",
      "3.4.1"
  ],
- "url": "https://bitbucket.org/mathematicalcoffee/xpenguins-gnome-shell-extension"
+ "url": "https://bitbucket.org/mathematicalcoffee/xpenguins-gnome-shell-extension",
+ "dev-version": "1.0"
 }

File xpenguins@mathematical.coffee.gmail.com/stylesheet.css

+.xpenguins-icon {
+}
+
+/* About dialog */
 .xpenguins-about-title {
     font-weight: bold;
     font-size: 1.5em;
+    padding-left: 0.4em;
+    padding-right: 0.4em;
+}
+
+.xpenguins-about-icon {
+    icon-size: 1.5em;
 }
 
 .xpenguins-about-text {
+    font-size: 1.2em;
 }
+
+
+/* Theme slider items */
+.theme-slider-menu-item-top-box {
+    padding-left: 0em;
+    padding-right: 1.75em;
+}
+
+.theme-slider-menu-item-bottom-box {
+    padding-left: 0em;
+    padding-right: 0em;
+}

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

  * xpenguins_theme.c
  *********************/
 /* Imports */
+const Lang  = imports.lang;
 const Shell = imports.gi.Shell;
 
 const Gettext = imports.gettext.domain('gnome-shell-extensions');
 const _ = Gettext.gettext;
 
 const Me = imports.ui.extensionSystem.extensions['xpenguins@mathematical.coffee.gmail.com'];
-const ThemeManager = Me.themeManager.ThemeManager;
+const ThemeManager = Me.themeManager;
 const Toon   = Me.toon;
 const WindowListener = Me.windowListener;
 const XPenguins = Me.xpenguins;
 
 Theme.prototype = {
     _init: function (themeList) {
-        XPUtil.DEBUG('theme');
+        XPUtil.DEBUG('creating theme');
         /* members */
         /* Theme: can have one or more genera
          * Genus: class of toons (Penguins has 2: walker & skateboarder).
          * Each genus has toon types: walker, floater, tumbler, faller, ...
-         *
-         * this.toonData: array, one per genus (per theme). this.toonData[i] = { type_of_toon: ToonData }
-         * this.number: array of numbers, one per genus
          */
-        this.toonData = []; // data, one per genus
-        this.number = [];   // theme penguin numbers
-        this.nactions = []; /* Number of random actions the genus has (type actionX) */
+        /* per genus in each theme */
+        this.toonData = {}; // [genus][type], where genus == <Theme_Genus||1>.
+        this.nactions = {};
+        this.number   = {};
+        /* per theme */
+        this._themeGenusMap = {}; // theme => [genus1, genus2]
+        this.totalsPerTheme = {}; //per THEME.
+
+        /* global across all themes/genii */
         this.delay = 60;
+        this.total = 0;
 
         /* Initialise */
+        themeList = themeList || [];
         for (let i = 0; i < themeList.length; ++i) {
             XPUtil.DEBUG(' ... appending theme %s', themeList[i]);
             this.appendTheme(themeList[i]);
         }
     }, // _init
 
-    get ngenera() {
-        return this.number.length;
+    hasTheme: function (iname) {
+        return this._themeGenusMap[ThemeManager.sanitiseThemeName(iname)] !== undefined;
     },
 
-    get total() {
-        return Math.min(XPenguins.PENGUIN_MAX, this.number.reduce(function (x, y) { return x + y; }));
+    /* get genus names for a theme as an array */
+    getGeniiForTheme: function (iname) {
+        return this._themeGenusMap[ThemeManager.sanitiseThemeName(iname)] || [];
+    },
+
+    /* gets numbers per genus for a theme as an array. */
+    getGenusNumbersForTheme: function (iname) {
+        let name = ThemeManager.sanitiseThemeName(iname);
+        if (!this._themeGenusMap[name]) {
+            return [];
+        }
+        return this._themeGenusMap[name].map(Lang.bind(this, function (genus) {
+            return this.number[genus];
+        }));
+    },
+
+    /* get number of toons for that theme */
+    getTotalForTheme: function (iname) {
+        let name = ThemeManager.sanitiseThemeName(iname);
+        return this.totalsPerTheme[name] || 0;
+    },
+
+    removeTheme: function (iname) {
+        /* Note: we do not actually delete this.toonData[genus_names] etc
+         * because we want on-the-fly theme swapping, meaning toons that
+         * are in the "dead" genus should explode using their old genus data
+         * and then respawn with the new genus data.
+         * They'll need to have this.toonData[genus_name] for that.
+         */
+        let name = ThemeManager.sanitiseThemeName(iname);
+        if (!this._themeGenusMap[name]) {
+            return;
+        }
+        this.total -= this.number[genus];
+        this.number[genus] = 0;
     },
 
     /* Append the theme named "name" to this theme. */
         let name = ThemeManager.sanitiseThemeName(iname),
             file_name = ThemeManager.getThemePath(name);
         if (!file_name) {
-            throw new Error('Theme ' + name + ' not found or config file not present');
+            throw new Error("Theme " + name + " not found or config file not present");
         }
+        /* if theme has already been parsed, do not re-parse */
+        if (this._themeGenusMap[name]) {
+            XPUtil.warn("Warning: theme %s already exists, not re-parsing", 
+                iname);
+            return;
+        }
+
 
         /* Read config file, ignoring comments ('#') and whitespace */
         let words = Shell.get_file_contents_utf8_sync(file_name),
-            started = false, // whether we've encountered the 'toon' keyword yet
-                             // (may be omitted in single-genera files)
-            first_genus = this.ngenera,
-            genus = this.ngenera,
+            added_genii = [],
+            genus = name + '_1',
             current,    // holds the current ToonData
             def = {},   // holds default ToonData
-            dummy = {}; // any unknown ToonData
+            dummy = {}, // any unknown ToonData
+            gdata,      // various looping variables
+            itype,
+            igenus;
 
         /* iterate through the words to parse the config file. */
         words = words.replace(/#.+/g, '');
         words = words.replace(/\s+/g, ' ');
+        /* Note: the 'toon' word is optional in one-genus themes.
+         * If the 'toon' word is present there must be a genus name
+         * so no need to use the default '_1'.
+         */
+        if (!words.match(/\btoon\b/)) {
+            this.grow(genus, name);
+            added_genii.push(genus);
+        }
         words = words.trim().split(' ');
 
-        /* make space for the next toon */
-        this.grow();
-
         try {
             for (let i = 0; i < words.length; ++i) {
                 let word = words[i].toLowerCase();
-                /* define a new genus of toon (walker, skateboarder, ...) 
-                 * note: the 'toon' word is optional in one-genus themes.
-                 * If we've already seen the 'toon' word before this must be a
-                 *  multi-genus theme so make space for it & increment 'genus' index.
-                 */
                 if (word === 'toon') {
-                    if (started) {
-                        this.grow();
-                        ++genus;
-                    } else {
-                        // first toon in file, don't have to ++genus.
-                        started = 1;
-                    }
                     /* store the genus index with the theme name */
-                    ++i;
+                    genus = name + '_' + words[++i];
+                    this.grow(genus, name); // will abort if alredy exists
+                    added_genii.push(genus);
                 } else if (word === 'delay') {
                 /* preferred frame delay in milliseconds */
                     this.delay = parseInt(words[++i], 10);
                         current = def;
                     } else if (type.match(/^(walker|faller|tumbler|floater|climber|runner|action[0-5]|exit|explosion|splatted|squashed|zapped|angel)$/)) {
                     /* other types of toon */
-                        started = 1;
                         /* note: passed by reference. */
                         this.toonData[genus][type] = new Toon.ToonData(def);
                         current = this.toonData[genus][type];
                             this.nactions[genus]++;
                         }
                     } else {
-                        XPUtil.warn(_("Warning: unknown type '%s': ignoring".format(type)));
+                        XPUtil.warn(_("Warning: unknown type '%s': ignoring"),
+                            type);
                         current = dummy;
                     }
                     /* extra configuration */
 
                         /* Pixmap is already defined! */
                         if (current.texture) {
-                            XPUtil.warn(_("Warning: resetting pixmap to %s".format(pixmap)));
+                            XPUtil.warn(_("Warning: resetting pixmap to %s"),
+                                pixmap);
                             /* Free old pixmap if it is not a copy */
                             if (!current.master) {
                                 current.texture.destroy();
                             }
                         }
 
-                        /* Check if the file has been used before */
-                        let new_pixmap = 1;
-                        for (let igenus = first_genus; igenus <= genus && new_pixmap; ++igenus) {
-                            let data = this.toonData[igenus];
-                            for (let itype in data) {
+                        /* Check if the file has been used before, but only 
+                         * look in the genii for the current theme */
+                        let new_pixmap = true;
+                        for (igenus = 0; igenus < added_genii.length && new_pixmap; ++igenus) {
+                            gdata = this.toonData[added_genii[igenus]];
+                            for (itype in gdata) {
                                 /* data already exists in theme, set master */
-                                if (data.hasOwnProperty(itype) && data[itype].filename &&
-                                        !data[itype].master && data[itype].filename === pixmap) {
+                                if (gdata.hasOwnProperty(itype) && 
+                                        gdata[itype].filename &&
+                                        !gdata[itype].master && 
+                                        gdata[itype].filename === pixmap) {
                                          // set .master & .texture (& hence .filename)
-                                    current.setMaster(data[itype]);
-                                    new_pixmap = 0;
+                                    current.setMaster(gdata[itype]);
+                                    new_pixmap = false;
                                     break;
                                 }
                             }
                     this.number[genus] = parseInt(words[++i], 10);
                 } else {
                 /* unknown word */
-                    XPUtil.warn(_("Warning: Unrecognised word %s, ignoring".format(word)));
+                    XPUtil.warn(_("Warning: Unrecognised word %s, ignoring"),
+                        word);
                 }
             } // while read word
         } catch (err) {
-            throw new Error(_("Error reading config file: config file ended unexpectedly: Line " + err.lineNumber + ": " + err.message));
+            XPUtil.error(
+                _("Error reading config file: config file ended unexpectedly: Line %d: %s"),
+                err.lineNumber, err.message); // throws error
+            return;
         } /* end config file parsing */
 
         /* Now valid our widths, heights etc with the size of the image
          * for all the types of the genera we just added
          */
-        for (let i = first_genus; i < this.ngenera; ++i) {
-            for (let j in this.toonData[i]) {
-                if (this.toonData[i].hasOwnProperty(j)) {
-                    current = this.toonData[i][j];
+        igenus = added_genii.length;
+        while (igenus--) {
+            gdata = this.toonData[added_genii[igenus]];
+            if (!gdata.walker || !gdata.faller) {
+                throw new Error(_("Theme must contain at least walkers and fallers"));
+            }
+            for (itype in gdata) {
+                if (gdata.hasOwnProperty(itype)) {
+                    current = gdata[itype];
                     let imwidth = current.texture.width,
                         imheight = current.texture.height;
                     if ((current.nframes = imwidth / current.width) < 1) {
                         if (imwidth < current.width) {
                             throw new Error(_("Width of xpm image too small for even a single frame"));
                         } else {
-                            XPUtil.warn(_("Warning: width of %s is too small to display all frames".format(
+                            XPUtil.warn(_("Warning: width of %s is too small to display all frames"),
                                 current.filename
-                            )));
+                            );
                         }
                     }
                     if (imheight < current.height * current.ndirections) {
                         if ((current.ndirections = imheight / current.height) < 1) {
                             throw new Error(_("Height of xpm image too small for even a single frame"));
                         } else {
-                            XPUtil.warn(_("Warning: height of %s is too small to display all frames".format(
+                            XPUtil.warn(_("Warning: height of %s is too small to display all frames"),
                                 current.filename
-                            )));
+                            );
                         }
                     }
                 }
             } // loop through Toon type
-            if (!this.toonData[i].walker || !this.toonData[i].faller) {
-                throw new Error(_("Theme must contain at least walkers and fallers"));
-            }
-        }
+            this.total += this.number[added_genii[igenus]];
+            this.totalsPerTheme[name] += this.number[added_genii[igenus]];
+        } // loop through added genii
     },  // appendTheme
 
-    grow: function () {
-        this.nactions.push(0);
-        this.number.push(1);
-        this.toonData.push({}); // object 'toonType': ToonData
+    grow: function (genus, themeName) {
+        if (this.toonData[genus]) {
+            return;
+        }
+        this.toonData[genus] = {};
+        this.nactions[genus] = 0;
+        this.number[genus] = 1;
+
+        if (!this._themeGenusMap[themeName]) {
+            this._themeGenusMap[themeName] = [];
+        }
+        this._themeGenusMap[themeName].push(genus);
+        this.totalsPerTheme[themeName] = 0;
     },
 
     destroy: function () {
         }
     }
 };
-

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

 let extensionPath = imports.ui.extensionSystem.extensionMeta['xpenguins@mathematical.coffee.gmail.com'].path;
 
 /***********************
- * ThemeManager object *
+ * ThemeManager        *
  ***********************/
-const ThemeManager = {
-    /* Look for themes in
-     * $HOME/.xpenguins/themes
-     * [xpenguins_directory]/themes
-     */
-    _themeDirectory: 'themes',
-    _systemDirectory: extensionPath,
-    _userDirectory: '.xpenguins',
-    _configFile: 'config',
+/* Look for themes in
+ * $HOME/.xpenguins/themes
+ * [xpenguins_directory]/themes
+ */
+const _themeDirectory = 'themes';
+const _systemDirectory = extensionPath;
+const _userDirectory = '.xpenguins';
+const _configFile = 'config';
 
-    sanitiseThemeName: function (name) {
-        return name.replace(/ /g,'_');
-    },
+function sanitiseThemeName(name) {
+    return name.replace(/ /g,'_');
+}
 
-    prettyThemeName: function (name) {
-        return name.replace(/_/g, ' ');
-    },
+function prettyThemeName(name) {
+    return name.replace(/_/g, ' ');
+}
 
+/* xpenguins_list_themes */
+/* Return a list of names of apparently valid themes -
+ * basically the directory names from either the user or the system
+ * theme directories that contain a file called "config" are
+ * returned. Underscores in the directory names are converted into
+ * spaces, but directory names that already contain spaces are
+ * rejected. This is because the install program seems to choke on
+ * directory names containing spaces, but theme names containing
+ * underscores are ugly.
+ */
+function listThemes() {
+    let themes_dir, info, fileEnum, i,
+        themeList = [],
+        paths = [
+            GLib.build_filenamev([
+                GLib.get_home_dir(), 
+                this._userDirectory, 
+                this._themeDirectory]),
+            GLib.build_filenamev([
+                this._systemDirectory,
+                this._themeDirectory]) 
+        ];
+    for (i = 0; i < paths.length; ++i) {
+        themes_dir = Gio.file_new_for_path(paths[i]);
+        if (!themes_dir.query_exists(null)) {
+            continue;
+        }
 
-    /* xpenguins_list_themes */
-    /* Return a list of names of apparently valid themes -
-     * basically the directory names from either the user or the system
-     * theme directories that contain a file called "config" are
-     * returned. Underscores in the directory names are converted into
-     * spaces, but directory names that already contain spaces are
-     * rejected. This is because the install program seems to choke on
-     * directory names containing spaces, but theme names containing
-     * underscores are ugly.
-     */
-    listThemes: function () {
-        let themes_dir, info, fileEnum, i,
-            themeList = [],
-            paths = [ GLib.build_filenamev([ GLib.get_home_dir(), this._userDirectory, this._themeDirectory ]),
-                      GLib.build_filenamev([ this._systemDirectory, this._themeDirectory ]) ];
-        for (i = 0; i < paths.length; ++i) {
-            themes_dir = Gio.file_new_for_path(paths[i]);
-            if (!themes_dir.query_exists(null)) {
+        fileEnum = themes_dir.enumerate_children('standard::*',
+                Gio.FileQueryInfoFlags.NONE, null);
+        while ((info = fileEnum.next_file(null)) !== null) {
+            let configFile = GLib.build_filenamev([themes_dir.get_path(),
+                                                    info.get_name(),
+                                                    this._configFile]);
+            if (GLib.file_test(configFile, GLib.FileTest.EXISTS)) {
+                themeList.push(info.get_name());
+            }
+        }
+        fileEnum.close(null);
+    } // loop through system & local xpenguins dir.
+
+  /* We convert all underscores in the directory name
+   * to spaces, but actual spaces in the directory
+   * name are not allowed. */
+    themeList = themeList.filter(function (x) { return !x.match(' '); });
+    themeList = themeList.map(function (x) { return prettyThemeName(x); });
+
+    /* remove duplicates */
+    themeList = XPUtil.removeDuplicates(themeList);
+
+    return themeList;
+}
+
+/* xpenguins_theme_info (xpenguins_theme.c)
+ * DescribeThemes (main.c)
+ */
+function describeThemes(themes) {
+    let theme, loc, i,
+        th = themes.length,
+        infos = {};
+    while (th--) {
+        theme = sanitiseThemeName(themes[th]);
+        loc = this.getThemePath(theme, 'about');
+        infos[theme] = {};
+        if (!loc || !GLib.file_test(loc, GLib.FileTest.EXISTS)) {
+            XPUtil.warn('Theme %s not found', theme);
+            continue;
+        }
+
+        /* parse the theme.
+         * xpenguins_theme_info */
+
+        /* Read about file, ignoring comments ('#'), double spaces */
+        let lines = Shell.get_file_contents_utf8_sync(loc);
+        lines = lines.replace(/#.+/g, '');
+        lines = lines.replace(/ {2,}/g, ' ');
+        lines = lines.split(/[\r\n]+/);
+
+        /* get first word & then rest of line. */
+        i = lines.length;
+        while (i--) {
+            let line = lines[i].trim();
+            if (line.length === 0) {
                 continue;
             }
+            let j = line.indexOf(' '),
+                word = line.slice(0, j).toLowerCase(),
+                rest = line.slice(j + 1);
 
-            fileEnum = themes_dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
-            while ((info = fileEnum.next_file(null)) !== null) {
-                let configFile = GLib.build_filenamev([themes_dir.get_path(),
-                                                        info.get_name(),
-                                                        this._configFile]);
-                if (GLib.file_test(configFile, GLib.FileTest.EXISTS)) {
-                    themeList.push(info.get_name());
+            if (word.match(/^(artists?|maintainer|date|copyright|license|comment)$/)) {
+                infos[theme][word] = rest;
+            } else if (word === 'icon') {
+                if (rest[0] !== '/') { /* make full path */
+                    rest = loc.replace(/\babout$/, rest);
                 }
-            }
-            fileEnum.close(null);
-        } // loop through system & local xpenguins dir.
-
-      /* We convert all underscores in the directory name
-       * to spaces, but actual spaces in the directory
-       * name are not allowed. */
-        themeList = themeList.filter(function (x) { return !x.match(' '); });
-        themeList = themeList.map(function (x) { return x.replace(/_/g, ' '); });
-
-        /* remove duplicates */
-        themeList = XPUtil.removeDuplicates(themeList);
-
-        return themeList;
-    },
-
-    /* xpenguins_theme_info (xpenguins_theme.c)
-     * DescribeThemes (main.c)
-     */
-    describeThemes: function (themes) {
-        let theme, loc, i,
-            th = themes.length,
-            infos = {};
-        while (th--) {
-            theme = themes[th].replace(/ /g, '_');
-            loc = this.getThemePath(theme, 'about');
-            infos[theme] = {};
-            if (!loc || !GLib.file_test(loc, GLib.FileTest.EXISTS)) {
-                XPUtil.warn('Theme %s not found', theme);
-                continue;
-            }
-
-            /* parse the theme.
-             * xpenguins_theme_info */
-
-            /* Read about file, ignoring comments ('#'), double spaces */
-            let lines = Shell.get_file_contents_utf8_sync(loc);
-            lines = lines.replace(/#.+/g, '');
-            lines = lines.replace(/ {2,}/g, ' ');
-            lines = lines.split(/[\r\n]+/);
-
-            /* get first word & then rest of line. */
-            i = lines.length;
-            while (i--) {
-                let line = lines[i].trim();
-                if (line.length === 0) {
-                    continue;
-                }
-                let j = line.indexOf(' '),
-                    word = line.slice(0, j).toLowerCase(),
-                    rest = line.slice(j + 1);
-
-                if (word.match(/^(artists?|maintainer|date|copyright|license|comment)$/)) {
-                    infos[theme][word] = rest;
-                } else if (word === 'icon') {
-                    if (rest[0] !== '/') { /* make full path */
-                        rest = loc.replace(/\babout$/, rest);
-                    }
-                    infos[theme][word] = rest;
-                } else {
-                    /* silently skip? */
-                    XPUtil.LOG('unrecognised word %s, silently skipping', word);
-                }
-            }
-
-            infos[theme].name = theme.replace(/_/g, ' ');;
-            infos[theme].sanitised_name = theme;
-            infos[theme].location = loc;
-        } // theme loop
-
-        return infos;
-    },
-
-    /* Return the full path or directory of the specified theme.
-     * Spaces in theme name are converted to underscores
-     * xpenguins_theme_directory
-     * It returns the *directory* name if the theme is "valid", i.e. contains a file 'config'.
-     */
-    getThemeDir: function (iname) {
-        /* Convert spaces to underscores */
-        /* first look in $HOME/.xpenguins/themes for config,
-         * then in [xpenguins_dir]/themes
-         */
-        let name = iname.replace(/ /g, '_'),
-            dirs = [ GLib.build_filenamev([ GLib.get_home_dir(), this._userDirectory, this._themeDirectory, name ]),
-                      GLib.build_filenamev([ this._systemDirectory, this._themeDirectory, name ]) ];
-        for (let i = 0; i < dirs.length; ++i) {
-            if (GLib.file_test(GLib.build_filenamev([dirs[i], this._configFile]),
-                                GLib.FileTest.EXISTS)) {
-                return dirs[i];
+                infos[theme][word] = rest;
+            } else {
+                /* silently skip? */
+                XPUtil.LOG('unrecognised word %s, silently skipping', word);
             }
         }
 
-        /* Theme not found */
-        return null;
-    },
+        infos[theme].name = prettyThemeName(theme);
+        infos[theme].sanitised_name = theme;
+        infos[theme].location = loc;
+    } // theme loop
 
-    getThemePath: function (iname, fName) {
-        let dir = this.getThemeDir(iname);
-        return GLib.build_filenamev([dir, fName || this._configFile]);
+    return infos;
+}
+
+/* Return the full path or directory of the specified theme.
+ * Spaces in theme name are converted to underscores
+ * xpenguins_theme_directory
+ * It returns the *directory* name if the theme contains a file 'config'.
+ */
+function getThemeDir(iname) {
+    /* Convert spaces to underscores */
+    /* first look in $HOME/.xpenguins/themes for config,
+     * then in [xpenguins_dir]/themes
+     */
+    let name = sanitiseThemeName(iname),
+        dirs = [GLib.build_filenamev([
+                    GLib.get_home_dir(), 
+                    this._userDirectory, 
+                    this._themeDirectory, 
+                    name]),
+                GLib.build_filenamev([
+                      this._systemDirectory, 
+                      this._themeDirectory, 
+                      name])
+               ];
+    for (let i = 0; i < dirs.length; ++i) {
+        if (GLib.file_test(GLib.build_filenamev([dirs[i], this._configFile]),
+                            GLib.FileTest.EXISTS)) {
+            return dirs[i];
+        }
     }
-}; // ThemeManager
 
+    /* Theme not found */
+    return null;
+}
+
+function getThemePath(iname, fName) {
+    let dir = this.getThemeDir(iname);
+    return GLib.build_filenamev([dir, fName || this._configFile]);
+}
+

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

         this.genus = null;
         this.type = 'faller';
         this.direction = null;
+        this.theme = null;
 
         /* toon is associated with a window */
         this.associate = UNASSOCIATED; 
             }
         }
         this.actor.set_position(0, 0);
-        if (this.genus !== null) {
+        if (this.genus) {
             this.init();
         }
 
     },
 
     /* Only call this *after* setting the toon's genus */
-    init: function () {
-        XPUtil.DEBUG(('TOON.INIT: genus: ' + this.genus + ' type: ' + this.type));
+    init: function (genus) {
+        if (genus) {
+            this.genus = genus;
+        }
         this.data = this._globals.toonData[this.genus][this.type];
         this.direction = XPUtil.RandInt(2);
         this.setType('faller', this.direction, UNASSOCIATED);
-        this.actor.set_position(XPUtil.RandInt(this._globals.XPenguinsWindow.get_width() - this.data.width), 1 - this.data.height);
+        this.actor.set_position(
+            XPUtil.RandInt(this._globals.XPenguinsWindow.get_width() - this.data.width),
+            1 - this.data.height);
         this.setAssociation(UNASSOCIATED);
         this.setVelocity(this.direction * 2 - 1, this.data.speed);
         this.terminating = false;
      */
     offsetBlocked: function (xoffset, yoffset) {
         if (this._globals.edge_block) {
-            if ((this.x + xoffset <= 0)
-                    || (this.x + this.data.width + xoffset >= this._globals.XPenguinsWindow.get_width())
-                    || ((this.y + yoffset <= 0) && this._globals.edge_block !== SIDEBOTTOMBLOCK)
-                    || (this.y + this.data.height + yoffset >= this._globals.XPenguinsWindow.get_height())) {
+            if ((this.x + xoffset <= 0) ||
+                    (this.x + this.data.width + xoffset >= 
+                        this._globals.XPenguinsWindow.get_width()) ||
+                    ((this.y + yoffset <= 0) && this._globals.edge_block 
+                        !== SIDEBOTTOMBLOCK) ||
+                    (this.y + this.data.height + yoffset 
+                        >= this._globals.XPenguinsWindow.get_height())) {
                 return true;
             }
         }
-        return this._globals.toon_windows.overlaps(this.x + xoffset, this.y + yoffset,
-                    this.data.width, this.data.height);
+        return this._globals.toon_windows.overlaps(this.x + xoffset, 
+            this.y + yoffset, this.data.width, this.data.height);
     },
 
     /* Check to see if a toon would be squashed instantly if changed to
     checkBlocked: function (type, gravity) {
         let newpos = this.calculateNewPosition(this.genus, type, gravity),
             newdata = this._globals.toonData[this.genus][type];
-        return this._globals.toon_windows.overlaps(newpos[0], newpos[1], newdata.width, newdata.height);
+        return this._globals.toon_windows.overlaps(newpos[0], newpos[1], 
+            newdata.width, newdata.height);
     },
 
     /**** MORPHING FUNCTIONS ****/
     /* Turn a penguin into a climber */
     // __xpenguins_make_climber
     makeClimber: function () {
-        this.setType('climber', this.direction, (this.direction ? DOWNRIGHT : DOWNLEFT));
+        this.setType('climber', this.direction,
+            (this.direction ? DOWNRIGHT : DOWNLEFT));
         this.setAssociation(this.direction);
         this.setVelocity(0, -this.data.speed);
     },
         this.setAssociation(UNASSOCIATED);
     },
 
-    /**** HANDLING TOON ASSOCIATIONS WITH MOVING WINDOWS (toon_associate.c) ****/
+    /*** HANDLING TOON ASSOCIATIONS WITH MOVING WINDOWS (toon_associate.c) ***/
     /* The first thing to be done when the windows move is to work out
        which windows the associated toons were associated with just before
        the windows moved
                 width = 1;
                 height = this.data.height;
             } else {
-                throw new Error(_("Error: illegal direction %d".format(this.associate)));
+                throw new Error(_("Error: illegal direction %d"), this.associate);
             } // switch(this.associate)
             this.wid = -1;
 
             if (newx < 0) {
                 newx = 0;
                 result = PARTIALMOVE;
-            } else if (newx + this.data.width > this._globals.XPenguinsWindow.get_width()) {
+            } else if (newx + this.data.width > 
+                this._globals.XPenguinsWindow.get_width()) {
                 newx = this._globals.XPenguinsWindow.get_width() - this.data.width;
                 result = PARTIALMOVE;
             }
                 if (newy < 0 && this._globals.edge_block !== SIDEBOTTOMBLOCK) {
                     newy = 0;
                     result = PARTIALMOVE;
-                } else if (newy + this.data.height > this._globals.XPenguinsWindow.get_height()) {
+                } else if (newy + this.data.height >
+                    this._globals.XPenguinsWindow.get_height()) {
                     newy = this._globals.XPenguinsWindow.get_height() - this.data.height;
                     result = PARTIALMOVE;
                 }
             }
 
             /* Is new toon location fully/partially filled with windows? */
-            if (this._globals.toon_windows.overlaps(newx, newy, this.data.width, this.data.height) && mode === MOVE &&
+            if (this._globals.toon_windows.overlaps(newx, newy, this.data.width,
+                    this.data.height) && mode === MOVE &&
                     result !== BLOCKED && !stationary) {
                 let tryx, tryy,
                     step = 1,
                     }
                     for (tryx = newx + step; tryx !== this.x; tryx += step) {
                         tryy = this.y + (tryx - this.x) * v / u;
-                        if (!this._globals.toon_windows.overlaps(tryx, tryy, this.data.width, this.data.height)) {
+                        if (!this._globals.toon_windows.overlaps(tryx, tryy,
+                                this.data.width, this.data.height)) {
                             newx = tryx;
                             newy = tryy;
                             result = PARTIALMOVE;
                     }
                     for (tryy = newy + step; tryy !== this.y; tryy += step) {
                         tryx = this.x + (tryy - this.y) * u / v;
-                        if (!this._globals.toon_windows.overlaps(tryx, tryy, this.data.width, this.data.height)) {
+                        if (!this._globals.toon_windows.overlaps(tryx, tryy,
+                                this.data.width, this.data.height)) {
                             newx = tryx;
                             newy = tryy;
                             result = PARTIALMOVE;
                 }
             }
         } /* what sort of blocking to consider */
-        //XPUtil.DEBUG('toon.advance: moving from (%d,%d) to (%d,%d)'.format(this.x, this.y, newx, newy));
         if (move_ahead) {
             this.actor.set_position(newx, newy);
             // see if we've scrolled to the end of the filmstrip
     draw: function () {
         /* Draw the toon on */
         if (this.active) {
-            let direction = (this.direction >= this.data.ndirections ? 0 : this.direction),
+            let direction = (this.direction >= this.data.ndirections ? 0 : 
+                    this.direction),
                 anchor_x = this.data.width * this.frame,
                 anchor_y = this.data.height * direction;
 
     /* __xpenguins_copy_properties */
     _init: function (otherToonData) {
         /* Properties: set default values */
-        this.conf = DEFAULTS;      /* bitmask of toon properties such as cycling etc */
-        this.texture = null; /* Clutter.Texture, replaces .image, .mask and .pixmap */
-
-        // .master is needed to make sure all clones point to the one same source.
-        this.master = null;             /* If pixmap data is duplicated from another toon, this is it */
-        this.nframes = 0;               /* number of frames in image */
-        this.ndirections = 1;           /* number directions in image (1 or 2) */
-        this.width = this.height = 30;  /* width & height of individual frame/dir */
+        this.conf = DEFAULTS;// bitmask of toon properties such as cycling etc 
+        this.texture = null; // Clutter.Texture
+        this.master = null;  // If pixmap data is duplicated from another toon, 
+                             //  this is it .
+        this.nframes = 0;    // number of frames in image 
+        this.ndirections = 1;           // number directions in image (1 or 2) 
+        this.width = this.height = 30;  // width & height of individual frame
         this.acceleration = this.terminal_velocity = 0;
         this.speed = 4;
-        this.loop = 0;                  /* Number of times to repeat cycle */
+        this.loop = 0;                  // Number of times to repeat cycle 
 
         /* Copy select properties from otherToonData to here. */
         let propListToCopy = ['nframes', 'ndirections', 'width', 'height',

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

     let msg = LOG.apply(null, arguments);
     global.log(msg);
 }
+
+function error() {
+    let msg = LOG.apply(null, arguments);
+    global.log(msg);
+    throw new Error(msg);
+}

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

 /*
  * An object that tests the firing/connecting of all the signals.
  * For debugging.
- * Not to be included in the final extension.
  */
 const XPenguins = {
     RECALC: {
         ALWAYS: 0,
-        PAUSE : 1,
-        END   : 2
+        END   : 1,
+        PAUSE : 2
     }
 };
 
-/* Returns a list of XPenguins features that are supported by your version of gnome-shell.
- * Default returns a whitelist (i.e. list.opt == TRUE means supported).
- * Otherwise, you can specificy a blacklist (list.opt == TRUE means blacklisted).
+/* Returns a list of XPenguins features that are supported by your version 
+ * of gnome-shell. 
+ * By default, returns a whitelist (i.e. list.opt TRUE means supported).
+ * Otherwise, you can specificy a blacklist (list.opt TRUE means blacklisted).
  */
 function getCompatibleOptions(blacklist) {
     /* enable everything by default */
      * create an instance of it first */
     options: {
         ignorePopups: false,
+        ignoreMaximised: true,
+        ignoreHalfMaximised: true,
         recalcMode: XPenguins.RECALC.ALWAYS,
         onDesktop: true,
         onAllWorkspaces: false
     _init: function (i_options) {
          /*
           * Everyone:
-          * RESTACKING: either notify::focus-app OR {for each win: "raised"}
+          * RESTACKING: {for each win: "raised"}
           * NEW WINDOWS/DESTROYED WINDOWS:
-          *   IGNORE POPUPS: window-added and window-removed    for dirtying toon windows
-          *  !IGNORE POPUPS: mapped       and destroyed         for dirtying toon windows
+          *   IGNORE POPUPS: window-added and window-removed
+          *  !IGNORE POPUPS: mapped       and destroyed
           * WINDOW STATE:
           *  RECALC.PAUSE:  grab-op-{begin, end} (will miss keyboard-resizes)
           *                 maximize
           *                 minimize
           *  RECALC.ALWAYS: {for each winActor: allocation-changed}
           * UNMINIMISE:
-          *   IGNORE POPUPS: nothing <hope for focus-app. Otherwise, can try winActor:show or window:notify::minimized>
+          *   IGNORE POPUPS: {for each win: notify::minimized}
           *  !IGNORE POPUPS: nothing <mapped>
           *
-          * Anything with {foreach win} or {foreach winActor} needs to listen to window-added and workspace-switched.
+          * Anything with {foreach win} or {foreach winActor} needs to listen
+          * to window-added and workspace-switched.
           */
 
-        /* dummy stuff for XPenguinsLoop compatibility */
         for (let opt in i_options) {
             if (i_options.hasOwnProperty(opt) && this.options.hasOwnProperty(opt)) {
                 this.options[opt] = i_options[opt];
         }
 
         this._playing = false;
-        this._resumeSignal = {}; /* when you pause you have to listen for an event to unpause; use this._resumeID to store this. */
-        this._listeningPerWindow = false; /* whether we have to listen to individual windows for signals */
+        /* when you pause you have to listen for an event to unpause; 
+         * use this._resumeID to store this. */
+        this._resumeSignal = {}; 
+        /* whether we have to listen to individual windows for signals */
+        this._listeningPerWindow = false; 
 
         this._XPenguinsWindow = global.stage;
         let tmp = this.options.onAllWorkspaces;
      */
     pause: function (hide, owner, eventName, cb) {
         XPUtil.DEBUG('[WL] pause');
-        this._playing = false;
 
         /* recalculate toon windows on resume */
         this._dirtyToonWindows('pause');
     /* resumes timeline, connects up events */
     resume: function () {
         XPUtil.DEBUG('[WL] resume');
-        this._playing = true;
 
         if (this._drawingArea && !this._drawingArea.visible) {
             this._drawingArea.show();
     /* called when configuration is changed.
      */
     changeOption: function (propName, propVal) {
-        if (!this.options.hasOwnProperty(propName) || this.options[propName] === propVal) {
+        if (!this.options.hasOwnProperty(propName) || 
+                this.options[propName] === propVal) {
             return;
         }
 
         // ARGH compatibility issues....
         if (propName === 'onAllWorkspaces') {
             if (this._XPenguinsWindow instanceof Meta.WindowActor) {
-                this._XPenguinsWindow.get_workspace = this._XPenguinsWindow.meta_window.get_workspace;
+                this._XPenguinsWindow.get_workspace = 
+                    this._XPenguinsWindow.meta_window.get_workspace;
             } else {
                 /* just to initially connect the window's workspace to listen
                  * to window-added & window-removed events
                  */
                 if (this.options.onAllWorkspaces) {
                     // always return "current" workspace.
-                    this._XPenguinsWindow.get_workspace = Lang.bind(global.screen, global.screen.get_active_workspace);
+                    this._XPenguinsWindow.get_workspace = 
+                        Lang.bind(global.screen, global.screen.get_active_workspace);
                 } else {
                     // return the starting workspace.
                     let ws = global.screen.get_active_workspace();
-                    this._XPenguinsWindow.get_workspace = function () { return ws; };
+                    this._XPenguinsWindow.get_workspace = function () { 
+                        return ws; 
+                    };
                 }
             }
         }
 
     _connectSignals: function () {
         XPUtil.DEBUG('[WL] connectSignals');
-        this._listeningPerWindow = false; /* whether we have to listen to individual windows for signals */
+        this._listeningPerWindow = false;
         let ws = this._XPenguinsWindow.get_workspace();
 
         /* new or destroyed windows */
         if (this.options.ignorePopups) {
-            /* Listen to 'window-added' and '-removed': these are the only windows that count. */
-            this._connectAndTrack(this, ws, 'window-added', Lang.bind(this, function () { this._dirtyToonWindows('window-added'); }));
-            this._connectAndTrack(this, ws, 'window-removed', Lang.bind(this, function () { this._dirtyToonWindows('window-removed'); }));
+            /* Listen to 'window-added' and 'window-removed' */
+            this._connectAndTrack(this, ws, 'window-added', 
+                Lang.bind(this, function () {
+                    this._dirtyToonWindows('window-added'); 
+                }));
+            this._connectAndTrack(this, ws, 'window-removed',
+                Lang.bind(this, function () {
+                    this._dirtyToonWindows('window-removed');
+                }));
         } else {
             /* Listen to 'mapped' and 'destroyed': every window here counts */
-            this._connectAndTrack(this, global.window_manager, 'map', Lang.bind(this, function () { this._dirtyToonWindows('map'); }));
-            this._connectAndTrack(this, global.window_manager, 'destroy', Lang.bind(this, function () { this._dirtyToonWindows('destroy'); }));
+            this._connectAndTrack(this, global.window_manager, 'map', 
+                Lang.bind(this, function () { 
+                    this._dirtyToonWindows('map'); 
+                }));
+            this._connectAndTrack(this, global.window_manager, 'destroy', 
+                Lang.bind(this, function () { 
+                    this._dirtyToonWindows('destroy'); 
+                }));
         }
 
 
 
         /* maximize, unmaximize */
         if (this.options.recalcMode !== XPenguins.RECALC.ALWAYS) {
-            this._connectAndTrack(this, global.window_manager, 'maximize', Lang.bind(this, function () { this._dirtyToonWindows('maximize'); }));
-            this._connectAndTrack(this, global.window_manager, 'unmaximize', Lang.bind(this, function () { this._dirtyToonWindows('unmaximize'); }));
+            this._connectAndTrack(this, global.window_manager, 'maximize', 
+                Lang.bind(this, function () { 
+                    this._dirtyToonWindows('maximize'); 
+                }));
+            this._connectAndTrack(this, global.window_manager, 'unmaximize', 
+                Lang.bind(this, function () { 
+                    this._dirtyToonWindows('unmaximize'); 
+                }));
         }   /* Otherwise allocation-changed covers all of the above. */
 
         /* minimize/unminimize */
         } else {
             /* Otherwise 'map' covers unminimize */
             if (this.options.recalcMode !== XPenguins.RECALC.ALWAYS) {
-                this._connectAndTrack(this, global.window_manager, 'minimize', Lang.bind(this, function () { this._dirtyToonWindows('minimize'); }));
+                this._connectAndTrack(this, global.window_manager, 'minimize', 
+                    Lang.bind(this, function () { 
+                        this._dirtyToonWindows('minimize'); 
+                    }));
             } /* Otherwise 'allocation-changed' covers minimize. */
         }
 
-        /* stacking order: NOTE: this *only* matters if we are not running on the desktop, or
-         * if we are ignoring maximised windows (& windows underneath them) - must remember them when
-         * they become visible.
-         * Just listen to notify::raise on all windows (notify::focus-app fires twice).
+        /* stacking order: NOTE: this *only* matters if we are not running on 
+         * the desktop, or if we are ignoring maximised windows (& windows 
+         * underneath them) - must remember them when they become visible.
+         * Just listen to notify::raise on all windows 
+         * (notify::focus-app fires twice so not that one.).
          */
         if (!this.options.onDesktop || this.options.ignoreMaximised) {
             this._listeningPerWindow = true;
             /* done in _onWindowAdded */
         }
 
-        /*** if listening to any events from each window, we need to listen to window-added and window-removed
-             in order to add the appropriate listeners.
-             Then, we also need to listen to workspace-changed to reconnect these signals.
-         ***/
+        /* if listening to any events from each window, we need to listen to 
+         * window-added and window-removed in order to add the appropriate 
+         * listeners. Then, we also need to listen to workspace-changed to 
+         * reconnect these signals.
+         */
         if (this._listeningPerWindow) {
-            this._connectAndTrack(this, ws, 'window-added', Lang.bind(this, this._onWindowAdded));
-            this._connectAndTrack(this, ws, 'window-removed', Lang.bind(this, this._onWindowRemoved));
+            this._connectAndTrack(this, ws, 'window-added', 
+                Lang.bind(this, this._onWindowAdded));
+            this._connectAndTrack(this, ws, 'window-removed', 
+                Lang.bind(this, this._onWindowRemoved));
 
-            this._connectAndTrack(this, global.window_manager, 'switch-workspace', Lang.bind(this, this._onWorkspaceChanged));
+            this._connectAndTrack(this, global.window_manager, 
+                'switch-workspace', Lang.bind(this, this._onWorkspaceChanged));
             /* connect up existing windows */
             ws.list_windows().map(Lang.bind(this, function (metaWin) {
                 if (metaWin.get_window_type() !== Meta.WindowType.DESKTOP) {
                 }));
         }
 
-        /* Stacking order. If we're not running on the desktop, then listen to 'raised' */
+        /* Stacking order. 
+         * If we're not running on the desktop, then listen to 'raised' */
         if (!this.options.Desktop || this.options.ignoreMaximised) {
-            this._connectAndTrack(winActor, metaWin, 'raised', Lang.bind(this, function () {
-                this._dirtyToonWindows('raised');
-            }));
+            this._connectAndTrack(winActor, metaWin, 'raised', 
+                Lang.bind(this, function () {
+                    this._dirtyToonWindows('raised');
+                }));
         }
 
         /* resized/moved windows */
         if (this.options.recalcMode === XPenguins.RECALC.ALWAYS) {
-            this._connectAndTrack(winActor, winActor, 'allocation-changed', Lang.bind(this, function () {
-                this._dirtyToonWindows('allocation-changed');
-            }));
+            this._connectAndTrack(winActor, winActor, 'allocation-changed', 
+                Lang.bind(this, function () {
+                    this._dirtyToonWindows('allocation-changed');
+                }));
         }
     },
 
     _onWorkspaceChanged: function (shellwm, fromI, toI, direction) {
         // from & to are indices.
         XPUtil.DEBUG('_onWorkspaceChanged: from %d to %d', fromI, toI);
-        /* If you've changed workspaces, you need to change window-added/removed listeners. */
+        /* If you've changed workspaces, you need to change window-added/
+         * removed listeners. */
         if (this.options.onAllWorkspaces) {
-            /* update the toon region */
-            // Note: if you call this straight away and switch back into a workspace *with* windows, it doesn't update until the next event.
-            // However, if you're running a timeline it'll be fine.
+            /* update the toon region
+             * Note: if you call this straight away and switch back into a 
+             * workspace *with* windows, it doesn't update until the next event.
+             * However, if you're running a timeline it'll be fine. */
             Mainloop.idle_add(Lang.bind(this, function () {
                 this._dirtyToonWindows('_onWorkspaceChanged');
                 return false;
             }));
 
-            /* disconnect/reconnect window-added & window-removed events we were listening to */
+            /* disconnect/reconnect window-added & window-removed events 
+             * we were listening to */
             if (this._listeningPerWindow) {
                 let from = global.screen.get_workspace_by_index(fromI),
                     to = global.screen.get_workspace_by_index(toI);
                 this._disconnectTrackedSignals(from);
 
-                this._connectAndTrack(this, to, 'window-added', Lang.bind(this, this._onWindowAdded));
-                this._connectAndTrack(this, to, 'window-removed', Lang.bind(this, this._onWindowRemoved));
+                this._connectAndTrack(this, to, 'window-added', 
+                    Lang.bind(this, this._onWindowAdded));
+                this._connectAndTrack(this, to, 'window-removed', 
+                    Lang.bind(this, this._onWindowRemoved));
 
                 /* connect up existing windows */
                 to.list_windows().map(Lang.bind(this, function (metaWin) {
             if (global.screen.get_workspace_by_index(toI) !==
                     this._XPenguinsWindow.get_workspace()) {
                 this.pause(true, global.window_manager, 'switch-workspace',
-                    /* Note: binding done on pause end. do it here too for safety? */
                     function (dmy, fI, tI, dir) {
                         return (global.screen.get_workspace_by_index(tI) === 
                             this._XPenguinsWindow.get_workspace());
             winList = ws.list_windows();
         } else {
             // already sorted.
-            winList = global.get_window_actors().map(function (act) { return act.meta_window; });
+            winList = global.get_window_actors().map(function (act) { 
+                return act.meta_window; 
+            });
             /* filter out other workspaces */
-            winList = winList.filter(function (win) { return win.get_workspace() === ws; });
+            winList = winList.filter(function (win) { 
+                return win.get_workspace() === ws; 
+            });
         }
 
         /* sort by stacking (if !onDesktop or ignoreMaximised).
          * Supposedly global.get_window_actors() is already sorted by stacking order
          * but sometimes it needs a Mainloop.idle_add before it works properly.
-         * If I resort them it all seems to go fine.
+         * If I re-sort them it all seems to go fine.
          */
         if (!this.options.onDesktop || this.options.ignoreMaximised) {
             winList = global.display.sort_windows_by_stacking(winList);
         }
 
-        /* iterate through backwards: every window up to winList[i] == winActor has a chance
-         * of being on top of you. Once you hit winList[i] == winActor, the other windows
-         * are *guaranteed* to be behind you.
+        /* iterate through backwards: every window up to winList[i] == winActor 
+         * has a chance of being on top of you. Once you hit winList[i] == 
+         * winActor, the other windows are *guaranteed* to be behind you.
          */
         /* filter out desktop & nonvisible/mapped windows windows */
         winList = winList.filter(Lang.bind(this, function (win) {
         let i = winList.length;
         while (i--) {
             /* exit once you hit the window actor (if !onDesktop),
-             * or once you hit a maximised window (if ignoreMaximised) 
+             * or once you hit a fully-maximised window (if ignoreMaximised) 
+             * or once you hit a half-maximised window (if ignoreMax & halfMax)
              */ 
-            if ((!this.options.onDesktop && winList[i] === this._XPenguinsWindow.meta_window) ||
-                    (this.options.ignoreMaximised && winList[i].get_maximized() ===
-                        (Meta.MaximizeFlags.HORIZONTAL | Meta.MaximizeFlags.VERTICAL))) {
+            let max = winList[i].get_maximized();
+            if ((!this.options.onDesktop && winList[i] ===