/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */

const Big = imports.gi.Big;
const Clutter = imports.gi.Clutter;
const GdkPixbuf = imports.gi.GdkPixbuf;
const Gdk = imports.gi.Gdk;
const Gtk = imports.gi.Gtk;
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 WORKSPACE_SWITCH_TIME = 0.25;
// Note that mutter has a compile-time limit of 36
const MAX_WORKSPACES = 16;

const GRID_SPACING = 15;
const SINGLE_VIEW_SPACING = 25;

const WorkspacesViewType = {
    SINGLE: 0,
    MOSAIC: 1
};

function GenericWorkspacesView(width, height, x, y, animate) {
    this._init(width, height, x, y, animate);
}

GenericWorkspacesView.prototype = {
    _init: function(width, height, x, y, animate) {
        this.actor = new St.Bin({ style_class: "workspaces" });
        this._actor = new Clutter.Group();

        this.actor.add_actor(this._actor);

        this._width = width;
        this._height = height;
        this._x = x;
        this._y = y;

        this._windowSelectionAppId = null;

        this._workspaces = [];

        this._highlightWindow = null;

        let activeWorkspaceIndex = global.screen.get_active_workspace_index();

        // Create and position workspace objects
        for (let w = 0; w < global.screen.n_workspaces; w++) {
            this._addWorkspaceActor(w);
        }
        this._workspaces[activeWorkspaceIndex].actor.raise_top();
        this._positionWorkspaces();

        // Position/scale the desktop windows and their children after the
        // workspaces have been created. This cannot be done first because
        // window movement depends on the Workspaces object being accessible
        // as an Overview member.
        this._overviewShowingId =
            Main.overview.connect('showing',
                                 Lang.bind(this, function() {
                this._onRestacked();
                for (let w = 0; w < this._workspaces.length; w++)
                    this._workspaces[w].zoomToOverview(animate);
        }));

        // Track changes to the number of workspaces
        this._nWorkspacesNotifyId =
            global.screen.connect('notify::n-workspaces',
                                  Lang.bind(this, this._workspacesChanged));
        this._switchWorkspaceNotifyId =
            global.window_manager.connect('switch-workspace',
                                          Lang.bind(this, this._activeWorkspaceChanged));
        this._restackedNotifyId =
            global.screen.connect('restacked',
                                  Lang.bind(this, this._onRestacked));
    },

    _lookupWorkspaceForMetaWindow: function (metaWindow) {
        for (let i = 0; i < this._workspaces.length; i++) {
            if (this._workspaces[i].containsMetaWindow(metaWindow))
                return this._workspaces[i];
        }
        return null;
    },

    _lookupCloneForMetaWindow: function (metaWindow) {
        for (let i = 0; i < this._workspaces.length; i++) {
            let clone = this._workspaces[i].lookupCloneForMetaWindow(metaWindow);
            if (clone)
                return clone;
        }
        return null;
    },

    setHighlightWindow: function (metaWindow) {
        // Looping over all workspaces is easier than keeping track of the last
        // highlighted window while trying to handle the window or workspace possibly
        // going away.
        for (let i = 0; i < this._workspaces.length; i++) {
            this._workspaces[i].setHighlightWindow(null);
        }
        if (metaWindow != null) {
            let workspace = this._lookupWorkspaceForMetaWindow(metaWindow);
            workspace.setHighlightWindow(metaWindow);
        }
    },

    _clearApplicationWindowSelection: function(reposition) {
        if (this._windowSelectionAppId == null)
            return;
        this._windowSelectionAppId = null;

        for (let i = 0; i < this._workspaces.length; i++) {
            this._workspaces[i].setLightboxMode(false);
            this._workspaces[i].setShowOnlyWindows(null, reposition);
        }
    },

    /**
     * setApplicationWindowSelection:
     * @appid: Application identifier string
     *
     * Enter a mode which shows only the windows owned by the
     * given application, and allow highlighting of a specific
     * window with setHighlightWindow().
     */
    setApplicationWindowSelection: function (appId) {
        if (appId == null) {
            this._clearApplicationWindowSelection(true);
            return;
        }

        if (appId == this._windowSelectionAppId)
            return;

        this._windowSelectionAppId = appId;

        let appSys = Shell.AppSystem.get_default();

        let showOnlyWindows = {};
        let app = appSys.get_app(appId);
        let windows = app.get_windows();
        for (let i = 0; i < windows.length; i++) {
            showOnlyWindows[windows[i]] = 1;
        }

        for (let i = 0; i < this._workspaces.length; i++) {
            this._workspaces[i].setLightboxMode(true);
            this._workspaces[i].setShowOnlyWindows(showOnlyWindows, true);
        }
    },

    /**
     * activateWindowFromOverview:
     * @metaWindow: A #MetaWindow
     * @time: Integer even timestamp
     *
     * This function exits the overview, switching to the given @metaWindow.
     * If an application filter is in effect, it will be cleared.
     */
    activateWindowFromOverview: function (metaWindow, time) {
        if (this._windowSelectionAppId != null) {
            this._clearApplicationWindowSelection(false);
        }

        Main.activateWindow(metaWindow, time);
        Main.overview.hide();
    },

    hide: function() {
        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
        let activeWorkspace = this._workspaces[activeWorkspaceIndex];

        activeWorkspace.actor.raise_top();

        for (let w = 0; w < this._workspaces.length; w++)
            this._workspaces[w].zoomFromOverview();
    },

    destroy: function() {
        for (let w = 0; w < this._workspaces.length; w++)
            this._workspaces[w].destroy();
        this._workspaces = [];

        this.actor.destroy();
        this.actor = null;

        Main.overview.disconnect(this._overviewShowingId);
        global.screen.disconnect(this._nWorkspacesNotifyId);
        global.window_manager.disconnect(this._switchWorkspaceNotifyId);
        global.screen.disconnect(this._restackedNotifyId);
    },

    getScale: function() {
        return this._workspaces[0].scale;
    },

    _onRestacked: function() {
        let stack = global.get_windows();
        let stackIndices = {};

        for (let i = 0; i < stack.length; i++) {
            // Use the stable sequence for an integer to use as a hash key
            stackIndices[stack[i].get_meta_window().get_stable_sequence()] = i;
        }

        for (let i = 0; i < this._workspaces.length; i++)
            this._workspaces[i].syncStacking(stackIndices);
    },

    // Handles a drop onto the (+) button; assumes the new workspace
    // has already been added
    acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) {
        return this._workspaces[this._workspaces.length - 1].acceptDrop(source, dropActor, x, y, time);
    },

    // Get the grid position of the active workspace.
    getActiveWorkspacePosition: function() {
        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
        let activeWorkspace = this._workspaces[activeWorkspaceIndex];

        return [activeWorkspace.gridX, activeWorkspace.gridY];
    },

    _setButtonSensitivity: function(button, sensitive) {
        if (button == null)
            return;
        if (sensitive && !button.reactive) {
            button.reactive = true;
            button.opacity = 255;
        } else if (!sensitive && button.reactive) {
            button.reactive = false;
            button.opacity = 85;
        }
    },

    createControllerBar: function() {
        throw new Error("Not implemented");
    },

    _positionWorkspaces: function() {
        throw new Error("Not implemented");
    },

    _workspacesChanged: function() {
        throw new Error("Not implemented");
    },

    _activeWorkspaceChanged: function() {
        throw new Error("Not implemented");
    },

    _addWorkspaceActor: function() {
        throw new Error("Not implemented");
    }
}

function MosaicView(width, height, x, y, animate) {
    this._init(width, height, x, y, animate);
}

MosaicView.prototype = {
    __proto__: GenericWorkspacesView.prototype,

    _init: function(width, height, x, y, animate) {
        GenericWorkspacesView.prototype._init.call(this, width, height, x, y, animate);

        this._workspaces[global.screen.get_active_workspace_index()].setSelected(true);

        this._removeButton = null;
        this._addButton = null;
    },

    // Assign grid positions to workspaces. We can't just do a simple
    // row-major or column-major numbering, because we don't want the
    // existing workspaces to get rearranged when we add a row or
    // column. So we alternate between adding to rows and adding to
    // columns. (So, eg, when going from a 2x2 grid of 4 workspaces to
    // a 3x2 grid of 5 workspaces, the 4 existing workspaces stay
    // where they are, and the 5th one is added to the end of the
    // first row.)
    //
    // FIXME: need to make the metacity internal layout agree with this!
    _positionWorkspaces: function() {
        let gridWidth = Math.ceil(Math.sqrt(this._workspaces.length));
        let gridHeight = Math.ceil(this._workspaces.length / gridWidth);

        let wsWidth = (this._width - (gridWidth - 1) * GRID_SPACING) / gridWidth;
        let wsHeight = (this._height - (gridHeight - 1) * GRID_SPACING) / gridHeight;
        let scale = wsWidth / global.screen_width;

        let span = 1, n = 0, row = 0, col = 0, horiz = true;

        for (let w = 0; w < this._workspaces.length; w++) {
            let workspace = this._workspaces[w];

            workspace.gridRow = row;
            workspace.gridCol = col;

            workspace.gridX = this._x + workspace.gridCol * (wsWidth + GRID_SPACING);
            workspace.gridY = this._y + workspace.gridRow * (wsHeight + GRID_SPACING);
            workspace.scale = scale;

            if (horiz) {
                col++;
                if (col == span) {
                    row = 0;
                    horiz = false;
                }
            } else {
                row++;
                if (row == span) {
                    col = 0;
                    horiz = true;
                    span++;
                }
            }
        }
    },

    _workspacesChanged: function() {
        let oldNumWorkspaces = this._workspaces.length;
        let newNumWorkspaces = global.screen.n_workspaces;

        if (oldNumWorkspaces == newNumWorkspaces)
            return;

        let oldScale = this._workspaces[0].scale;
        let oldGridWidth = Math.ceil(Math.sqrt(oldNumWorkspaces));
        let oldGridHeight = Math.ceil(oldNumWorkspaces / oldGridWidth);
        let lostWorkspaces = [];

        // The old last workspace is no longer removable.

        if (newNumWorkspaces > oldNumWorkspaces) {
            // Create new workspace groups
            for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
                this._addWorkspaceActor(w);
            }

        } else {
            // Truncate the list of workspaces
            // FIXME: assumes that the workspaces are being removed from
            // the end of the list, not the start/middle
            lostWorkspaces = this._workspaces.splice(newNumWorkspaces);
        }

        // The new last workspace may be removable
        let newLastWorkspace = this._workspaces[this._workspaces.length - 1];

        // Figure out the new layout
        this._positionWorkspaces();
        let newScale = this._workspaces[0].scale;
        let newGridWidth = Math.ceil(Math.sqrt(newNumWorkspaces));
        let newGridHeight = Math.ceil(newNumWorkspaces / newGridWidth);

        if (newGridWidth != oldGridWidth || newGridHeight != oldGridHeight) {
            // We need to resize/move the existing workspaces/windows
            let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces);
            for (let w = 0; w < existingWorkspaces; w++)
                this._workspaces[w].resizeToGrid(oldScale);
        }

        if (newScale != oldScale) {
            // The workspace scale affects window size/positioning because we clamp
            // window size to a 1:1 ratio and never scale them up
            let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces);
            for (let w = 0; w < existingWorkspaces; w++)
                this._workspaces[w].positionWindows(Workspace.WindowPositionFlags.ANIMATE);
        }

        if (newNumWorkspaces > oldNumWorkspaces) {
            // Slide new workspaces in from offscreen
            for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++)
                this._workspaces[w].slideIn(oldScale);
        } else {
            // Slide old workspaces out
            for (let w = 0; w < lostWorkspaces.length; w++) {
                let workspace = lostWorkspaces[w];
                workspace.slideOut(function () { workspace.destroy(); });
            }

            // FIXME: deal with windows on the lost workspaces
        }

        // Reset the selection state; if we went from > 1 workspace to 1,
        // this has the side effect of removing the frame border
        let activeIndex = global.screen.get_active_workspace_index();
        this._workspaces[activeIndex].setSelected(true);

        this._updateButtonsVisibility();
    },

    _activeWorkspaceChanged: function(wm, from, to, direction) {
        this._workspaces[from].setSelected(false);
        this._workspaces[to].setSelected(true);
    },

    _addWorkspaceActor: function(workspaceNum) {
        let workspace  = new Workspace.Workspace(workspaceNum, this._actor);
        this._workspaces[workspaceNum] = workspace;
        this._actor.add_actor(workspace.actor);
    },

    createControllerBar: function() {
        let actor = new St.BoxLayout({ 'pack-start': true });
        let bin = new St.Bin();
        let addButton = new St.Button({ style_class: "workspace-controls add" });
        this._addButton = addButton;
        addButton.connect('clicked', Lang.bind(this, this._addNewWorkspace));
        addButton._delegate = addButton;
        addButton._delegate.acceptDrop = Lang.bind(this, function(source, actor, x, y, time) {
            return this._acceptNewWorkspaceDrop(source, actor, x, y, time);
        });
        actor.add(bin, { x_align: St.Align.END });
        bin.set_child(addButton);
        bin.set_alignment(St.Align.END, St.Align.START);

        bin = new St.Bin();
        let removeButton = new St.Button({ style_class: "workspace-controls remove" });
        this._removeButton = removeButton;
        removeButton.connect('clicked', Lang.bind(this, function() {
            if (this._workspaces.length <= 1)
                return;
            global.screen.remove_workspace(this._workspaces[this._workspaces.length - 1]._metaWorkspace, global.get_current_time());
        }));
        actor.add(bin, { expand: true, x_fill: true, x_align: St.Align.END });
        this._updateButtonsVisibility();
        bin.set_child(removeButton);
        bin.set_alignment(St.Align.END, St.Align.START);

        return actor;
    },

    _updateButtonsVisibility: function() {
        let canRemove = (global.screen.n_workspaces > 1);
        let canAdd = (global.screen.n_workspaces < MAX_WORKSPACES);

        this._setButtonSensitivity(this._removeButton, canRemove);
        this._setButtonSensitivity(this._addButton, canAdd);
    },

    _addNewWorkspace: function() {
        global.screen.append_new_workspace(false, global.get_current_time());
    },

    _acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) {
        this._addNewWorkspace();
        return this.acceptNewWorkspaceDrop(source, dropActor, x, y, time);
    }
};

function SingleView(width, height, x, y, animate) {
    this._init(width, height, x, y, animate);
}

SingleView.prototype = {
    __proto__: GenericWorkspacesView.prototype,

    _init: function(width, height, x, y, animate) {
        this._scroll = null;
        GenericWorkspacesView.prototype._init.call(this, width, height, x, y, animate);

        this._actor.set_clip(x, y, width, height);
        this._addButton = null;
        this._removeButton = null;
        this._indicatorsPanel = null;
        this._indicatorsPanelWidth = null;

        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
        for (let w = 0; w < this._workspaces.length; w++) {
            if (w != activeWorkspaceIndex) {
                this._workspaces[w].actor.hide();
                continue;
            }
            this._workspaces[w].actor.show();
            this._workspaces[w]._windowOverlaysGroup.show();
        }
    },

    _positionWorkspaces: function() {
        let position = global.screen.get_active_workspace_index();
        let scale = this._width / global.screen_width;

        if (this._scroll != null)
            position = this._scroll.adjustment.value;
        let isInt = (Math.round(position) === position);

        for (let w = 0; w < this._workspaces.length; w++) {
            let workspace = this._workspaces[w];

            workspace.gridRow = 0;
            workspace.gridCol = 0;

            workspace.scale = scale;
            workspace.actor.set_scale(scale, scale);
            let _width = workspace.actor.width * scale;
            workspace.gridX = this._x
                              + (w - position) * (_width + SINGLE_VIEW_SPACING);
            workspace.gridY = this._y;
            workspace.actor.set_position(workspace.gridX, workspace.gridY);
            // show the overlay unconditionally first, so items get
            // positioned correctly, then hide if necessary
            workspace._windowOverlaysGroup.show();
            if (isInt) {
                if (this.actor.get_stage() != null)
                   workspace.positionWindows(0);
                if (w == position) {
                    workspace.actor.show();
                } else {
                    workspace._windowOverlaysGroup.hide();
                    workspace.actor.hide();
                }
            } else {
                workspace._windowOverlaysGroup.hide();
                if (Math.abs(w - position) <= 1)
                    workspace.actor.show();
                else
                    workspace.actor.hide();
            }
        }
    },

    _workspacesChanged: function() {
        let oldNumWorkspaces = this._workspaces.length;
        let newNumWorkspaces = global.screen.n_workspaces;

        if (oldNumWorkspaces == newNumWorkspaces)
            return;

        if (this._scroll != null) {
            let adj = this._scroll.get_adjustment();
            adj.upper = newNumWorkspaces;
            this._scroll.adjustment = adj;
        }
        let lostWorkspaces = [];

        if (newNumWorkspaces > oldNumWorkspaces) {
            // Create new workspace groups
            for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
                this._addWorkspaceActor(w);
                this._workspaces[w].actor.hide();
            }

        } else {
            for (let i = 0; i < this._workspaces.length; i++)
                this._workspaces[i].destroy();
            this._actor.remove_all();

            //Without this will be a lot of warnings
            this._actor.hide();

            this._workspaces = [];
            let activeWorkspaceIndex = global.screen.get_active_workspace_index();
            for (let w = 0; w < global.screen.n_workspaces; w++) {
                this._addWorkspaceActor(w);
                if (w == activeWorkspaceIndex) {
                    this._workspaces[w].actor.show();
                } else {
                    this._workspaces[w].actor.hide();
                }
            }
            this._actor.show();
        }
        this._positionWorkspaces();

        // Reset the selection state; if we went from > 1 workspace to 1,
        // this has the side effect of removing the frame border
        let activeIndex = global.screen.get_active_workspace_index();
        this._workspaces[activeIndex].actor.show();
        this._workspaces[activeIndex]._windowOverlaysGroup.show();

        this._updatePanelVisibility();
    },

    _activeWorkspaceChanged: function(wm, from, to, direction) {
        this._updatePanelVisibility();
        let showAnimation = true;

        if (this._scroll != null) {
            let adj = this._scroll.get_adjustment();
            if (Math.round(adj.value - to) != adj.value - to)
                showAnimation = false;
            if (adj.value - to == 0)
                showAnimation = false;
            adj.value = to;
            this._scroll.adjustment = adj;
        }
        if (showAnimation) {
            let fx;
            if (from > to) {
                fx = this._workspaces[0].actor.width;
            } else {
                fx = -this._workspaces[0].actor.width;
            }
            this._workspaces[from]._windowOverlaysGroup.hide();
            this._workspaces[to].actor.set_position(this._x - fx, this._workspaces[to].gridY);
            this._workspaces[to].actor.show();
            Tweener.addTween(this._workspaces[to].actor,
                             { x: this._x,
                               transition: 'easeOutQuad',
                               time: WORKSPACE_SWITCH_TIME
                              });

            Tweener.addTween(this._workspaces[from].actor,
                             { x: this._x + fx,
                               transition: 'easeOutQuad',
                               time: WORKSPACE_SWITCH_TIME,
                               onComplete: this._positionWorkspaces,
                               onCompleteScope: this
                              });
        } else
            this._positionWorkspaces();
    },

    _addWorkspaceActor: function(workspaceNum) {
        let workspace  = new Workspace.Workspace(workspaceNum, this._actor);
        this._actor.add_actor(workspace.actor);
        workspace._windowOverlaysGroup.hide();

        this._workspaces[workspaceNum] = workspace;
    },

    createControllerBar: function() {
        let panel = new St.BoxLayout({ 'pack-start': true, vertical: true });

        let actor = new St.BoxLayout({ 'pack-start': true });
        let adj = new St.Adjustment({ value: global.screen.get_active_workspace_index(),
                                      lower: 0,
                                      'page-increment': 1,
                                      'page-size': 1,
                                      'step-increment': 1,
                                      upper: this._workspaces.length });
        this._scroll = new St.ScrollBar({ adjustment: null, vertical: false, name: 'SwitchScroll' });

        this._scroll.connect('notify::adjustment', Lang.bind(this, function() {
            this._scroll.adjustment.connect('notify::value', Lang.bind(this, function () {
                if (Math.abs(Math.round(this._scroll.adjustment.value) - this._scroll.adjustment.value) < 0.1) {
                    this._scroll.adjustment.set_value (Math.round(this._scroll.adjustment.value));
                    this._workspaces[Math.round(this._scroll.adjustment.value)]._metaWorkspace.activate(global.get_current_time());
                } else
                    this._positionWorkspaces();
            }));
        }));
        this._scroll.adjustment = adj;

        let addButton = new St.Button({ style_class: "workspace-controls add" });
        this._addButton = addButton;
        addButton.connect('clicked', Lang.bind(this, this._addNewWorkspace));
        addButton._delegate = addButton;
        addButton._delegate.acceptDrop = Lang.bind(this, function(source, actor, x, y, time) {
            return this._acceptNewWorkspaceDrop(source, actor, x, y, time);
        });
        actor.add(addButton, {x_align: St.Align.END, y_align: St.Align.START, 'y-fill': false});

        let removeButton = new St.Button({ style_class: "workspace-controls remove" });
        this._removeButton = removeButton;
        removeButton.connect('clicked', Lang.bind(this, function() {
            if (this._workspaces.length <= 1)
                return;
            let index = global.screen.get_active_workspace_index();
            if (index == 0)
                return;
            global.screen.remove_workspace(this._workspaces[index]._metaWorkspace, global.get_current_time());
        }));
        actor.add(removeButton, { x_align: St.Align.END, y_align: St.Align.START, 'y-fill': false });
        this._updatePanelVisibility();

        panel.add(this._createPositionalIndicator(), {expand: true, 'x-fill': true, 'y-fill': true});
        panel.add(this._scroll, { expand: true,
                                  'x-fill': true,
                                  'y-fill': false,
                                  y_align: St.Align.START });
        actor.add(panel, {expand: true, 'x-fill': true, 'y-fill': true});

        return actor;
    },

    _addIndicatorClone: function(i, active) {
        let actor = new St.Button({ style_class: 'workspace-indicator' });
        if (active) {
            actor.style_class = 'workspace-indicator active';
        }
        actor.connect('button-release-event', Lang.bind(this, function() {
            if (this._workspaces[i] != undefined)
                this._workspaces[i]._metaWorkspace.activate(global.get_current_time());
        }));

       actor.connect('scroll-event', Lang.bind(this, function(actor, event) {
            let direction = event.get_scroll_direction();
            let activeWorkspaceIndex = global.screen.get_active_workspace_index();
            let numWorkspaces = global.screen.n_workspaces;
            if (direction == Clutter.ScrollDirection.DOWN && activeWorkspaceIndex < numWorkspaces - 1) {
                this._workspaces[activeWorkspaceIndex+1]._metaWorkspace.activate(global.get_current_time());
            } else if (direction == Clutter.ScrollDirection.UP && activeWorkspaceIndex > 0) {
                this._workspaces[activeWorkspaceIndex-1]._metaWorkspace.activate(global.get_current_time());
            }
        }));

        this._indicatorsPanel.add_actor(actor);

        let [a, spacing] = actor.get_theme_node().get_length('border-spacing', false);
        if (this._indicatorsPanelWidth < spacing * (i + 1) + actor.width * (i + 1))
            actor.hide();
        actor.x = spacing * i + actor.width * i;
    },

    _fillPositionalIndicator: function() {
        if (this._indicatorsPanel == null || this._indicatorsPanelWidth == null)
            return;
        let width = this._indicatorsPanelWidth;
        this._indicatorsPanel.remove_all();

        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
        for (let i = 0; i < this._workspaces.length; i++) {
            this._addIndicatorClone(i, i == activeWorkspaceIndex);
        }
        this._indicatorsPanel.x = (this._indicatorsPanelWidth - this._indicatorsPanel.width) / 2;
    },

    _createPositionalIndicator: function() {
        let actor = new St.Bin({ style_class: 'panel-button' });
        let group = new Clutter.Group();

        this._indicatorsPanel = new Shell.GenericContainer();
        this._indicatorsPanel.connect('get-preferred-width', Lang.bind(this, function (actor, fh, alloc) {
            let children = actor.get_children();
            let width = 0;
            for (let i = 0; i < children.length; i++) {
                if (!children[i].visible)
                    continue;
                if (children[i].x + children[i].width <= width)
                    continue;
                width = children[i].x + children[i].width;
            }
            alloc.min_size = width;
            alloc.natural_size = width;
        }));
        this._indicatorsPanel.connect('get-preferred-height', Lang.bind(this, function (actor, fw, alloc) {
            let children = actor.get_children();
            let height = 0;
            if (children.length)
                height = children[0].height;
            alloc.min_size = height;
            alloc.natural_size = height;
        }));
        this._indicatorsPanel.connect('allocate', Lang.bind(this, function (actor, box, flags) {
            let children = actor.get_children();
            for (let i = 0; i < children.length; i++) {
                if (!children[i].visible)
                    continue;
                let childBox = new Clutter.ActorBox();
                childBox.x1 = children[i].x;
                childBox.y1 = 0;
                childBox.x2 = children[i].x + children[i].width;
                childBox.y2 = children[i].height;
                children[i].allocate(childBox, flags);
            }
        }));

        group.add_actor(this._indicatorsPanel);
        actor.set_child(group);
        actor.set_alignment(St.Align.START, St.Align.START);
        actor.set_fill(true, true);
        this._indicatorsPanel.hide();
        actor.connect('notify::width', Lang.bind(this, function(actor) {
            this._indicatorsPanelWidth = actor.width;
            this._updatePanelVisibility();
        }));
        actor.connect('destroy', Lang.bind(this, function() {
            this._indicatorsPanel = null;
        }));
        return actor;
    },

    _updatePanelVisibility: function() {
        let canRemove = (global.screen.get_active_workspace_index() != 0);
        let canAdd = (global.screen.n_workspaces < MAX_WORKSPACES);

        this._setButtonSensitivity(this._removeButton, canRemove);
        this._setButtonSensitivity(this._addButton, canAdd);

        let showSwitches = (global.screen.n_workspaces > 1);
        if (this._scroll != null) {
            if (showSwitches)
                this._scroll.show();
            else
                this._scroll.hide();
        }
        if (this._indicatorsPanel != null) {
            if (showSwitches)
                this._indicatorsPanel.show();
            else
                this._indicatorsPanel.hide();
        }
        this._fillPositionalIndicator();
    },

    _addNewWorkspace: function() {
        // Button with opacity 0 is clickable.
        if (global.screen.n_workspaces >= MAX_WORKSPACES)
            return;
        global.screen.append_new_workspace(false, global.get_current_time());
        this._workspaces[this._workspaces.length - 1]._metaWorkspace.activate(Clutter.get_current_event_time());
    },

    _acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) {
        this._addNewWorkspace();
        return this.acceptNewWorkspaceDrop(source, dropActor, x, y, time);
    }
};

function WorkspacesViewSwitch() {
    this._init();
}

WorkspacesViewSwitch.prototype = {
    VIEW_KEY: 'view',

    _init: function() {
        this._gconf = Shell.GConf.get_default();
        this._mosaicViewButton = null;
        this._singleViewButton = null;
        this._currentViewType = this._gconf.get_int(this.VIEW_KEY);
        this._controlsBar = null;
    },

    _setView: function(view) {
        this._mosaicViewButton.set_checked(WorkspacesViewType.MOSAIC == view);
        this._singleViewButton.set_checked(WorkspacesViewType.SINGLE == view);

        if (this._currentViewType == view)
            return;
        this._currentViewType = view;
        this._gconf.set_int(this.VIEW_KEY, view);
        this.emit('view-changed');
    },

    createCurrentWorkspaceView: function(width, height, x, y, animate) {
        switch (this._currentViewType) {
            case WorkspacesViewType.SINGLE:
                return new SingleView(width, height, x, y, animate);
            case WorkspacesViewType.MOSAIC:
                return new MosaicView(width, height, x, y, animate);
            default:
                return new MosaicView(width, height, x, y, animate);
        }
    },

    createControlsBar: function() {
        let actor = new St.BoxLayout();

        this._mosaicViewButton = new St.Button({ style_class: "workspace-controls switch-mosaic" });
        this._mosaicViewButton.set_toggle_mode(true);
        this._mosaicViewButton.connect('clicked', Lang.bind(this, function() {
            this._setView(WorkspacesViewType.MOSAIC);
        }));
        actor.add(this._mosaicViewButton, {'y-fill' : false, 'y-align' : St.Align.START});

        this._singleViewButton = new St.Button({ style_class: "workspace-controls switch-single" });
        this._singleViewButton.set_toggle_mode(true);
        this._singleViewButton.connect('clicked', Lang.bind(this, function() {
            this._setView(WorkspacesViewType.SINGLE);
        }));
        actor.add(this._singleViewButton, {'y-fill' : false, 'y-align' : St.Align.START});

        if (this._currentViewType == WorkspacesViewType.MOSAIC)
            this._mosaicViewButton.set_checked(true);
        else
            this._singleViewButton.set_checked(true);

        this._nWorkspacesNotifyId =
            global.screen.connect('notify::n-workspaces',
                                  Lang.bind(this, this._workspacesChanged));

        actor.connect('destroy', Lang.bind(this, function() {
            this._controlsBar = null;
            global.screen.disconnect(this._nWorkspacesNotifyId);
        }));

        this._controlsBar = actor;
        this._workspacesChanged();
        return actor;
    },

    _workspacesChanged: function() {
        if (this._controlsBar == null)
            return;
        if (global.screen.n_workspaces == 1)
            this._controlsBar.set_opacity(0);
        else
            this._controlsBar.set_opacity(255);
    }
};

Signals.addSignalMethods(WorkspacesViewSwitch.prototype);