[gnome-shell-extensions] New extension: native-window-placement



commit f24365520c058e92ab2155bfdd3ea08726c8880e
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Thu May 12 18:42:53 2011 +0200

    New extension: native-window-placement
    
    This extension employs an algorithm (taken from KDE) for layouting
    the thumbnails in the overview that more closely reflects the positions
    and relative sizes of the actual windows, instead of using a fixed
    grid.

 configure.ac                                       |    5 +-
 extensions/native-window-placement/Makefile.am     |    3 +
 extensions/native-window-placement/extension.js    |  509 ++++++++++++++++++++
 .../native-window-placement/metadata.json.in       |    9 +
 extensions/native-window-placement/stylesheet.css  |    4 +
 5 files changed, 528 insertions(+), 2 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 3676db1..b18b56a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -23,7 +23,7 @@ dnl by default, install only extensions that do not change completely the shell
 dnl that don't require GSettings and that don't require external packages for typelibs
 dnl (so basically only menus, status icons, search providers, overview tabs, message tray sources, etc.)
 DEFAULT_EXTENSIONS="alternative-status-menu dock drive-menu gajim places-menu windowsNavigator"
-ALL_EXTENSIONS="$DEFAULT_EXTENSIONS alternate-tab auto-move-windows example systemMonitor user-theme xrandr-indicator"
+ALL_EXTENSIONS="$DEFAULT_EXTENSIONS alternate-tab auto-move-windows example native-window-placement systemMonitor user-theme xrandr-indicator"
 AC_SUBST(ALL_EXTENSIONS, [$ALL_EXTENSIONS])
 AC_ARG_ENABLE([extensions],
 	[AS_HELP_STRING([--enable-extensions],[Space separated list of extensions to enable.
@@ -50,7 +50,7 @@ for e in $enable_extensions; do
 					[AC_MSG_WARN([gnome-desktop-3.0 not found, disabling xrandr-indicator])])
 			;;
 dnl		keep this in alphabetic order
-		alternate-tab|alternative-status-menu|auto-move-windows|dock|drive-menu|example|gajim|places-menu|user-theme|windowsNavigator)
+		alternate-tab|alternative-status-menu|auto-move-windows|dock|drive-menu|example|gajim|native-window-placement|places-menu|user-theme|windowsNavigator)
 			ENABLED_EXTENSIONS="$ENABLED_EXTENSIONS $e"
 			;;
 		*)
@@ -71,6 +71,7 @@ AC_CONFIG_FILES([
   extensions/systemMonitor/Makefile
   extensions/windowsNavigator/Makefile
   extensions/gajim/Makefile
+  extensions/native-window-placement/Makefile
   extensions/places-menu/Makefile
   extensions/xrandr-indicator/Makefile
   extensions/user-theme/Makefile
diff --git a/extensions/native-window-placement/Makefile.am b/extensions/native-window-placement/Makefile.am
new file mode 100644
index 0000000..c53a614
--- /dev/null
+++ b/extensions/native-window-placement/Makefile.am
@@ -0,0 +1,3 @@
+EXTENSION_ID = native-window-placement
+
+include ../../extension.mk
diff --git a/extensions/native-window-placement/extension.js b/extensions/native-window-placement/extension.js
new file mode 100644
index 0000000..74006b0
--- /dev/null
+++ b/extensions/native-window-placement/extension.js
@@ -0,0 +1,509 @@
+// import just everything from workspace.js:
+const Clutter = imports.gi.Clutter;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Meta = imports.gi.Meta;
+const Pango = imports.gi.Pango;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+const Signals = imports.signals;
+
+const DND = imports.ui.dnd;
+const Lightbox = imports.ui.lightbox;
+const Main = imports.ui.main;
+const Overview = imports.ui.overview;
+const Panel = imports.ui.panel;
+const Tweener = imports.ui.tweener;
+
+const Workspace = imports.ui.workspace;
+const WindowPositionFlags = Workspace.WindowPositionFlags;
+
+// testing settings for natural window placement strategy:
+const WINDOW_PLACEMENT_STRATEGY = "natural"                         // window placement strategy: natural | grid
+const WINDOW_PLACEMENT_NATURAL_MORESCREEN = true;                   // activate mod4-directions-part that tries to use more screen real estate
+const WINDOW_PLACEMENT_NATURAL_FILLGAPS = true;                     // enlarge windows at the end to fill gaps         // not implemented yet
+const WINDOW_PLACEMENT_NATURAL_GRID_FALLBACK = true;                // fallback to grid mode if all windows have the same size and positions.     // not implemented yet
+const WINDOW_PLACEMENT_NATURAL_ACCURACY = 20;                       // accuracy of window translate moves  (KDE-default: 20)
+const WINDOW_PLACEMENT_NATURAL_GAPS = 5;                            // half of the minimum gap between windows
+const WINDOW_PLACEMENT_NATURAL_MAX_TRANSLATIONS = 5000;             // safety limit for preventing endless loop if something is wrong in the algorithm
+
+const PLACE_WINDOW_CAPTIONS_ON_TOP = true;                          // place window titles in overview on top of windows with overlap parameter
+
+function injectToFunction(parent, name, func) {
+    let origin = parent[name];
+    parent[name] = function() {
+        let ret;
+        ret = origin.apply(this, arguments);
+        if (ret === undefined)
+            ret = func.apply(this, arguments);
+        return ret;
+    }
+}
+
+
+ 
+function Rect(x, y, width, height) {
+    [this.x, this.y, this.width, this.height] = arguments;
+}
+
+Rect.prototype = {
+    /**
+     * used in _calculateWindowTransformationsNatural to replace Meta.Rectangle that is too slow.
+     */
+
+    copy: function() {
+        return new Rect(this.x, this.y, this.width, this.height);
+    },
+
+    union: function(rect2) {
+        let dest = this.copy();
+        if (rect2.x < dest.x)
+          {
+            dest.width += dest.x - rect2.x;
+            dest.x = rect2.x;
+          }
+        if (rect2.y < dest.y)
+          {
+            dest.height += dest.y - rect2.y;
+            dest.y = rect2.y;
+          }
+        if (rect2.x + rect2.width > dest.x + dest.width)
+          dest.width = rect2.x + rect2.width - dest.x;
+        if (rect2.y + rect2.height > dest.y + dest.height)
+          dest.height = rect2.y + rect2.height - dest.y;
+
+        return dest;
+    },
+
+    adjusted: function(dx, dy, dx2, dy2) {
+        let dest = this.copy();
+        dest.x += dx;
+        dest.y += dy;
+        dest.width += -dx + dx2;
+        dest.height += -dy + dy2;
+        return dest;
+    },
+
+    overlap: function(rect2) {
+        return !((this.x + this.width    <= rect2.x) ||
+                 (rect2.x + rect2.width  <= this.x) ||
+                 (this.y + this.height   <= rect2.y) ||
+                 (rect2.y + rect2.height <= this.y));
+    },
+
+    center: function() {
+        return [this.x + this.width / 2, this.y + this.height / 2];
+    },
+
+    translate: function(dx, dy) {
+        this.x += dx;
+        this.y += dy;
+    },
+
+};
+
+
+// Put your extension initialization code here
+function main() {
+
+
+    /**
+     * _calculateWindowTransformationsNatural:
+     * @clones: Array of #MetaWindow
+     *
+     * Returns clones with matching target coordinates and scales to arrange windows in a natural way that no overlap exists and relative window size is preserved.
+     * This function is almost a 1:1 copy of the function
+     * PresentWindowsEffect::calculateWindowTransformationsNatural() from KDE, see:
+     * https://projects.kde.org/projects/kde/kdebase/kde-workspace/repository/revisions/master/entry/kwin/effects/presentwindows/presentwindows.cpp
+     */
+    Workspace.Workspace.prototype._calculateWindowTransformationsNatural = function(clones) {
+        //global.log('_calculateWindowTransformationsNatural()');
+
+        // As we are using pseudo-random movement (See "slot") we need to make sure the list
+        // is always sorted the same way no matter which window is currently active.
+        clones = clones.sort(function (win1, win2) {
+            return win2.metaWindow.get_stable_sequence() - win1.metaWindow.get_stable_sequence();
+        });
+
+
+        // 2 * 10 px gaps at the border
+        let area = new Rect(this._x + 10, this._y + 10, this._width - 20, this._height - 20);
+
+        let bounds = area.copy();
+
+        let direction = 0;
+        let directions = [];
+        let rects = [];
+        for (let i = 0; i < clones.length; i++) {
+            // save rectangles into 4-dimensional arrays representing two corners of the rectangular: [left_x, top_y, right_x, bottom_y]
+            let rect = clones[i].metaWindow.get_outer_rect();
+            rects[i] = new Rect(rect.x, rect.y, rect.width, rect.height);
+            bounds = bounds.union(rects[i]);
+
+            // This is used when the window is on the edge of the screen to try to use as much screen real estate as possible.
+            directions[i] = direction;
+            direction++;
+            if (direction == 4) {
+                direction = 0;
+            }
+        }
+
+
+        let loop_counter = 0;
+        let overlap;
+        do {
+            overlap = false;
+            for (let i = 0; i < rects.length; i++) {
+                for (let j = 0; j < rects.length; j++) {
+                    if (i != j && rects[i].adjusted(-WINDOW_PLACEMENT_NATURAL_GAPS, -WINDOW_PLACEMENT_NATURAL_GAPS,
+                                                    WINDOW_PLACEMENT_NATURAL_GAPS, WINDOW_PLACEMENT_NATURAL_GAPS).overlap(
+                                                     rects[j].adjusted(-WINDOW_PLACEMENT_NATURAL_GAPS, -WINDOW_PLACEMENT_NATURAL_GAPS,
+                                                                       WINDOW_PLACEMENT_NATURAL_GAPS, WINDOW_PLACEMENT_NATURAL_GAPS)))
+                    {
+                        loop_counter++;
+                        overlap = true;
+
+                        //! @todo: something like a Point2D would be nicer here:
+
+                        // Determine pushing direction
+                        let i_center = rects[i].center();
+                        let j_center = rects[j].center();
+                        let diff = [j_center[0] - i_center[0], j_center[1] - i_center[1]];
+
+                        // Prevent dividing by zero and non-movement
+                        if (diff[0] == 0 && diff[1] == 0)
+                            diff[0] = 1;
+                        // Try to keep screen/workspace aspect ratio
+                        if ( bounds.height / bounds.width > area.height / area.width )
+                            diff[0] *= 2;
+                        else
+                            diff[1] *= 2;
+
+                        // Approximate a vector of between 10px and 20px in magnitude in the same direction
+                        let length = Math.sqrt(diff[0] * diff[0] + diff[1] * diff[1]);
+                        diff[0] = diff[0] * WINDOW_PLACEMENT_NATURAL_ACCURACY / length;
+                        diff[1] = diff[1] * WINDOW_PLACEMENT_NATURAL_ACCURACY / length;
+
+                        // Move both windows apart
+                        rects[i].translate(-diff[0], -diff[1]);
+                        rects[j].translate(diff[0], diff[1]);
+
+
+                        if (WINDOW_PLACEMENT_NATURAL_MORESCREEN)
+                        {
+                            // Try to keep the bounding rect the same aspect as the screen so that more
+                            // screen real estate is utilised. We do this by splitting the screen into nine
+                            // equal sections, if the window center is in any of the corner sections pull the
+                            // window towards the outer corner. If it is in any of the other edge sections
+                            // alternate between each corner on that edge. We don't want to determine it
+                            // randomly as it will not produce consistant locations when using the filter.
+                            // Only move one window so we don't cause large amounts of unnecessary zooming
+                            // in some situations. We need to do this even when expanding later just in case
+                            // all windows are the same size.
+                            // (We are using an old bounding rect for this, hopefully it doesn't matter)
+                            let xSection = Math.round((rects[i].x - bounds.x) / (bounds.width / 3));
+                            let ySection = Math.round((rects[i].y - bounds.y) / (bounds.height / 3));
+
+                            let i_center = rects[i].center();
+                            diff[0] = 0;
+                            diff[1] = 0;
+                            if (xSection != 1 || ySection != 1) { // Remove this if you want the center to pull as well
+                                if (xSection == 1)
+                                    xSection = (directions[i] / 2 ? 2 : 0);
+                                if (ySection == 1)
+                                    ySection = (directions[i] % 2 ? 2 : 0);
+                            }
+                            if (xSection == 0 && ySection == 0) {
+                                //diff = QPoint(bounds.topLeft() - targets[w].center());
+                                diff[0] = bounds.x - i_center[0];
+                                diff[1] = bounds.y - i_center[1];
+                            }
+                            if (xSection == 2 && ySection == 0) {
+                                //diff = QPoint(bounds.topRight() - targets[w].center());
+                                diff[0] = bounds.x + bounds.width - i_center[0];
+                                diff[1] = bounds.y - i_center[1];
+                            }
+                            if (xSection == 2 && ySection == 2) {
+                                //diff = QPoint(bounds.bottomRight() - targets[w].center());
+                                diff[0] = bounds.x + bounds.width - i_center[0];
+                                diff[1] = bounds.y + bounds.height - i_center[1];
+                            }
+                            if (xSection == 0 && ySection == 2) {
+                                //diff = QPoint(bounds.bottomLeft() - targets[w].center());
+                                diff[0] = bounds.x - i_center[0];
+                                diff[1] = bounds.y + bounds.height - i_center[1];
+                            }
+                            if (diff[0] != 0 || diff[1] != 0) {
+                                let length = Math.sqrt(diff[0]*diff[0] + diff[1]*diff[1]);
+                                diff[0] *= WINDOW_PLACEMENT_NATURAL_ACCURACY / length / 2;   // /2 to make it less influencing than the normal center-move above
+                                diff[1] *= WINDOW_PLACEMENT_NATURAL_ACCURACY / length / 2;
+                                rects[i].translate(diff[0], diff[1]);
+                            }
+                        }
+
+
+                        // Update bounding rect
+                        bounds = bounds.union(rects[i]);
+                        bounds = bounds.union(rects[j]);
+                    }
+                }
+            }
+        } while (overlap && loop_counter < WINDOW_PLACEMENT_NATURAL_MAX_TRANSLATIONS);
+
+        //global.log('loop finished.  ( windows processed, translations needed ) = ( ' + clones.length + ', ' + loop_counter + ' )' );
+
+
+        // Work out scaling by getting the most top-left and most bottom-right window coords.
+        // The 20's and 10's are so that the windows don't touch the edge of the screen.
+        // Most 20's and 10's are already calculated into area now.
+        let scale;
+        scale = Math.min( area.width / bounds.width,
+                          area.height / bounds.height,
+                          1.0 );
+
+        // Make bounding rect fill the screen size for later steps
+        bounds.x = bounds.x - (area.width - bounds.width * scale) / 2 - 10 / scale;
+        bounds.y = bounds.y - (area.height - bounds.height * scale) / 2 - 10 / scale;
+        bounds.width = area.width / scale;
+        bounds.height = area.height / scale;
+
+        // Move all windows back onto the screen and set their scale
+        for (let i = 0; i < rects.length; i++) {
+            rects[i].translate(-bounds.x, -bounds.y);
+        }
+
+        //! @todo: Implement the KDE part "Try to fill the gaps by enlarging windows if they have the space" here. (If this is wanted)
+
+
+        // rescale to workspace
+        let scales = [];
+
+        let buttonOuterHeight, captionHeight;
+        let buttonOuterWidth = 0;
+
+        let targets = [];
+        for (let i = 0; i < rects.length; i++) {
+            rects[i].x = rects[i].x * scale + this._x;
+            rects[i].y = rects[i].y * scale + this._y;
+
+            targets[i] = [rects[i].x, rects[i].y, scale];
+        }
+
+        return [clones, targets];
+    },
+
+    /**
+     * _calculateWindowTransformationsGrid:
+     * @clones: Array of #MetaWindow
+     *
+     * Returns clones with matching target coordinates and scales to arrange windows in a grid.
+     */
+    Workspace.Workspace.prototype._calculateWindowTransformationsGrid = function(clones) {
+        let slots = this._computeAllWindowSlots(clones.length);
+        clones = this._orderWindowsByMotionAndStartup(clones, slots);
+        let targets = [];
+
+        for (let i = 0; i < clones.length; i++) {
+            targets[i] = this._computeWindowLayout(clones[i].metaWindow, slots[i]);
+        }
+
+        return [clones, targets];
+    }
+
+
+
+    /**
+     * positionWindows:
+     * @flags:
+     *  INITIAL - this is the initial positioning of the windows.
+     *  ANIMATE - Indicates that we need animate changing position.
+     */
+     Workspace.Workspace.prototype.positionWindows = function(flags) {
+        if (this._repositionWindowsId > 0) {
+            Mainloop.source_remove(this._repositionWindowsId);
+            this._repositionWindowsId = 0;
+        }
+
+        let clones = this._windows.slice();
+        if (this._reservedSlot)
+            clones.push(this._reservedSlot);
+
+        let initialPositioning = flags & WindowPositionFlags.INITIAL;
+        let animate = flags & WindowPositionFlags.ANIMATE;
+
+        // Start the animations
+	let targets = [];
+        let scales = [];
+
+        switch (WINDOW_PLACEMENT_STRATEGY)
+        {
+        case "natural":
+            [clones, targets] = this._calculateWindowTransformationsNatural(clones);
+            break;
+        case "grid":
+        default:
+            [clones, targets] = this._calculateWindowTransformationsGrid(clones);
+            break;
+        }
+
+        let currentWorkspace = global.screen.get_active_workspace();
+        let isOnCurrentWorkspace = this.metaWorkspace == null || this.metaWorkspace == currentWorkspace;
+
+        for (let i = 0; i < clones.length; i++) {
+            let clone = clones[i];
+	    let [x, y , scale] = targets[i];
+            let metaWindow = clone.metaWindow;
+            let mainIndex = this._lookupIndex(metaWindow);
+            let overlay = this._windowOverlays[mainIndex];
+
+            // Positioning a window currently being dragged must be avoided;
+            // we'll just leave a blank spot in the layout for it.
+            if (clone.inDrag)
+                continue;
+
+            if (overlay)
+                overlay.hide();
+            if (animate && isOnCurrentWorkspace) {
+                if (!metaWindow.showing_on_its_workspace()) {
+                    /* Hidden windows should fade in and grow
+                     * therefore we need to resize them now so they
+                     * can be scaled up later */
+                     if (initialPositioning) {
+                         clone.actor.opacity = 0;
+                         clone.actor.scale_x = 0;
+                         clone.actor.scale_y = 0;
+                         clone.actor.x = x;
+                         clone.actor.y = y;
+                     }
+
+                     // Make the window slightly transparent to indicate it's hidden
+                     Tweener.addTween(clone.actor,
+                                      { opacity: 255,
+                                        time: Overview.ANIMATION_TIME,
+                                        transition: 'easeInQuad'
+                                      });
+                }
+
+                Tweener.addTween(clone.actor,
+                                 { x: x,
+                                   y: y,
+                                   scale_x: scale,
+                                   scale_y: scale,
+                                   time: Overview.ANIMATION_TIME,
+                                   transition: 'easeOutQuad',
+                                   onComplete: Lang.bind(this, function() {
+                                      this._showWindowOverlay(clone, overlay, true);
+                                   })
+                                 });
+            } else {
+                clone.actor.set_position(x, y);
+                clone.actor.set_scale(scale, scale);
+                this._showWindowOverlay(clone, overlay, isOnCurrentWorkspace);
+            }
+        }
+    }
+
+
+/// position window titles on top of windows in overlay ////
+
+  if ( PLACE_WINDOW_CAPTIONS_ON_TOP )  {
+   Workspace.WindowOverlay.prototype._init = function(windowClone, parentActor) {
+        let metaWindow = windowClone.metaWindow;
+
+        this._windowClone = windowClone;
+        this._parentActor = parentActor;
+        this._hidden = false;
+
+        let title = new St.Label({ style_class: 'window-caption',
+                                   text: metaWindow.title });
+        title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+        title._spacing = 0;
+        title._overlap = 0;
+
+        this._updateCaptionId = metaWindow.connect('notify::title',
+            Lang.bind(this, function(w) {
+                this.title.text = w.title;
+            }));
+
+        let button = new St.Button({ style_class: 'window-close' });
+        button._overlap = 0;
+
+        this._idleToggleCloseId = 0;
+        button.connect('clicked', Lang.bind(this, this._closeWindow));
+
+        windowClone.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+        windowClone.actor.connect('enter-event',
+                                  Lang.bind(this, this._onEnter));
+        windowClone.actor.connect('leave-event',
+                                  Lang.bind(this, this._onLeave));
+
+        this._windowAddedId = 0;
+        windowClone.connect('zoom-start', Lang.bind(this, this.hide));
+        windowClone.connect('zoom-end', Lang.bind(this, this.show));
+
+        button.hide();
+
+        this.title = title;
+        this.closeButton = button;
+
+        parentActor.add_actor(this.title);
+        parentActor.add_actor(this.closeButton);
+        title.connect('style-changed',
+                      Lang.bind(this, this._onStyleChanged));
+        button.connect('style-changed',
+                       Lang.bind(this, this._onStyleChanged));
+        // force a style change if we are already on a stage - otherwise
+        // the signal will be emitted normally when we are added
+        if (parentActor.get_stage())
+            this._onStyleChanged();
+    },
+
+  Workspace.WindowOverlay.prototype.chromeHeights = function () {
+        return [Math.max( this.closeButton.height - this.closeButton._overlap, this.title.height - this.title._overlap),
+               0];
+    },
+
+  Workspace.WindowOverlay.prototype.updatePositions = function(cloneX, cloneY, cloneWidth, cloneHeight) {
+        let button = this.closeButton;
+        let title = this.title;
+
+        let buttonX;
+        let buttonY = cloneY - (button.height - button._overlap);
+        if (St.Widget.get_default_direction() == St.TextDirection.RTL)
+            buttonX = cloneX - (button.width - button._overlap);
+        else
+            buttonX = cloneX + (cloneWidth - button._overlap);
+
+        button.set_position(Math.floor(buttonX), Math.floor(buttonY));
+
+        if (!title.fullWidth)
+            title.fullWidth = title.width;
+        title.width = Math.min(title.fullWidth, cloneWidth);
+
+        let titleX = cloneX + (cloneWidth - title.width) / 2;
+	let titleY = cloneY - title.height + title._overlap;
+        title.set_position(Math.floor(titleX), Math.floor(titleY));
+    },
+
+   Workspace.WindowOverlay.prototype._onStyleChanged = function() {
+        let titleNode = this.title.get_theme_node();
+        this.title._spacing = titleNode.get_length('-shell-caption-spacing');
+	this.title._overlap = titleNode.get_length('-shell-caption-overlap');
+
+        let closeNode = this.closeButton.get_theme_node();
+        this.closeButton._overlap = closeNode.get_length('-shell-close-overlap');
+
+        this._parentActor.queue_relayout();
+    }
+
+/*
+    injectToFunction(Workspace.WindowOverlay.prototype, '_init', function(windowClone, parentActor) {
+	title._overlap = 0;
+    });
+*/
+
+  }
+
+}
+
+
diff --git a/extensions/native-window-placement/metadata.json.in b/extensions/native-window-placement/metadata.json.in
new file mode 100644
index 0000000..237ffb5
--- /dev/null
+++ b/extensions/native-window-placement/metadata.json.in
@@ -0,0 +1,9 @@
+{
+ "uuid": "@uuid@",
+ "name": "Native Window Placement",
+ "description": "Arrange windows in overview in a more native way",
+ "shell-version": [ "@shell_current@" ],
+ "localedir": "@LOCALEDIR@",
+ "url": "@url@",
+ "original-authors": [ "wepmaschda gmx de" ]
+}
diff --git a/extensions/native-window-placement/stylesheet.css b/extensions/native-window-placement/stylesheet.css
new file mode 100644
index 0000000..a282ffd
--- /dev/null
+++ b/extensions/native-window-placement/stylesheet.css
@@ -0,0 +1,4 @@
+.window-caption {
+    color: red;
+    -shell-caption-overlap: 10px;
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]