/**
 * FILE:panel.js
 * @short_description: The file responsible for managing panels
 *
 * This file is where everything about panels happens. #Main will create a
 * #PanelManager object, which is responsible for creating and moving panels.
 * There is also a %checkPanelUpgrade function used as a transition between the
 * old panel settings and the new panel settings.
 */
const Cairo = imports.cairo;
const Clutter = imports.gi.Clutter;
const GObject = imports.gi.GObject;
const Mainloop = imports.mainloop;
const Meta = imports.gi.Meta;
const Pango = imports.gi.Pango;
const Cinnamon = imports.gi.Cinnamon;
const St = imports.gi.St;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Signals = imports.signals;

const Applet = imports.ui.applet;
const AppletManager = imports.ui.appletManager;
const DND = imports.ui.dnd;
const Main = imports.ui.main;
const ModalDialog = imports.ui.modalDialog;
const PopupMenu = imports.ui.popupMenu;
const SignalManager = imports.misc.signalManager;
const Util = imports.misc.util;

const BUTTON_DND_ACTIVATION_TIMEOUT = 250;

const AUTOHIDE_ANIMATION_TIME = 200;
const AUTOHIDE_BOX_FADE_TIME = 100;

const TIME_DELTA = 1500;

const PANEL_PEEK_TIME = 1500;

const EDIT_MODE_MIN_BOX_SIZE = 25;
const VALID_ICON_SIZE_VALUES = [-1, 0, 16, 22, 24, 32, 48];

/*** These are defaults for a new panel added */
const DEFAULT_PANEL_VALUES = {
    "panels-autohide": "false",
    "panels-show-delay": "0",
    "panels-hide-delay": "0",
    "panels-height": "40"
};

const DEFAULT_FULLCOLOR_ICON_SIZE_VALUES = {
    "left":   0,
    "center": 0,
    "right":  0
};

const DEFAULT_SYMBOLIC_ICON_SIZE_VALUES = {
    "left":   28,
    "center": 28,
    "right":  28
};

const MIN_SYMBOLIC_SIZE_PX = 10;
const MAX_SYMBOLIC_SIZE_PX = 50;

const DEFAULT_TEXT_SIZE_VALUES = {
    "left":   0.0,
    "center": 0.0,
    "right":  0.0
};

const MIN_TEXT_SIZE_PTS = 6.0;
const MAX_TEXT_SIZE_PTS = 16.0;

const PANEL_AUTOHIDE_KEY = "panels-autohide";
const PANEL_SHOW_DELAY_KEY = "panels-show-delay";
const PANEL_HIDE_DELAY_KEY = "panels-hide-delay";
const PANEL_HEIGHT_KEY = "panels-height";
const PANEL_ZONE_ICON_SIZES = "panel-zone-icon-sizes";
const PANEL_ZONE_SYMBOLIC_ICON_SIZES = "panel-zone-symbolic-icon-sizes";
const PANEL_ZONE_TEXT_SIZES = "panel-zone-text-sizes";

const Direction = {
    LEFT  : 0,
    RIGHT : 1
};

var PanelLoc = {
    top : 0,
    bottom : 1,
    left : 2,
    right : 3
};

const PanelDefElement = {
    ID  : 0,
    MONITOR : 1,
    POSITION: 2
};

/**
 * checkPanelUpgrade:
 *
 * Run from main, prior to PanelManager being initialized
 * this handles the one-time transition between panel implementations
 * to make this transition invisible to the user.  We will evaluate the
 * desktop-layout key, and pre-set applets-enabled and panels-enabled
 * appropriately.
 */
function checkPanelUpgrade()
{
    let oldLayout = global.settings.get_string("desktop-layout");

    let doIt = false;

    /* GLib >= 2.4 has get_user_value, use that if possible - this being null
     * indicates either the user never changed from the default "traditional"
     * panel layout, or else this upgrade has already been performed (since
     * with this set of patches, its default value goes from traditional to nothing.)
     * Either way, we don't need to do anything in this case.  With glib < 2.4,
     * we instead check if the value is set to "" - either by result of the new
     * default, or by this upgrade already having been run.
     */

    try {
        doIt = (global.settings.get_user_value("desktop-layout") != null)
    } catch (e) {
        doIt = (global.settings.get_string("desktop-layout") != "");
    }

    if (!doIt)
        return;

    switch (oldLayout) {
        case "flipped":
            setPanelsEnabledList(["1:0:top"]);
            break;
        case "classic":
            setPanelsEnabledList(["1:0:top", "2:0:bottom"]);
            break;
        case "traditional": /* Default (explicitly set) - no processing needed */
        default:
            break;
    }

    global.settings.reset("desktop-layout");
}

/**
 * heightsUsedMonitor:
 * @monitorIndex (integer): index of monitor
 * @listofpanels (array): array of panels
 *
 * Retrieves the heights used in horizontal panels on the monitor to that
 * vertical panels can be sized and positioned not to overlap them
 *
 * Returns: a two element array
 */

function heightsUsedMonitor(monitorIndex, listofpanels) {
    let toppanelHeight = 0;
    let bottompanelHeight = 0;

    for (let i = 0, len = listofpanels.length; i < len; i++) {
        if (listofpanels[i]) {
            if (listofpanels[i].monitorIndex == monitorIndex) {
                if (listofpanels[i].panelPosition == PanelLoc.top)
                    toppanelHeight = listofpanels[i].get_height();
                else if (listofpanels[i].panelPosition == PanelLoc.bottom)
                    bottompanelHeight = listofpanels[i].get_height();
            }
        }
    }
    return [toppanelHeight, bottompanelHeight];
};

/**
* getPanelLocFromName:
* @pname (char): panel type
*
* get the panel numeric type from its name in settings
*
* returns - panel type (integer)
*/
function getPanelLocFromName(pname) {
    // ensure something credible always returned even if supplied invalid data
    let jj = PanelLoc.bottom;
    switch (pname) {
        case "bottom":
            jj = PanelLoc.bottom;
            break;
        case "top":
            jj = PanelLoc.top;
            break;
        case "left":
            jj = PanelLoc.left;
            break;
        case "right":
            jj = PanelLoc.right;
            break;
    }
    return(jj);
};

/**
 * parsePanelDef:
 * @def (string): a panel definition from settings, in the form ID:monitor:panelposition
 *
 * Parses a single panel definition, rejecting one that doesn't have three elements, or
 * whose id or monitor index is negative or unparseable - such an index matches neither a
 * monitor nor a panel id, so the panel would be registered nowhere and its definition
 * never cleaned up. The position is not checked, getPanelLocFromName() maps anything it
 * doesn't recognise to PanelLoc.bottom.
 *
 * Returns: [id, monitor index, panel position], or null if the definition is unusable
 */
function parsePanelDef(def) {
    let elements = def.split(":");
    if (elements.length != 3)
        return null;

    let id = parseInt(elements[PanelDefElement.ID]);
    let monitorIndex = parseInt(elements[PanelDefElement.MONITOR]);
    let panelPosition = getPanelLocFromName(elements[PanelDefElement.POSITION]);

    if (isNaN(id) || id < 0 || isNaN(monitorIndex) || monitorIndex < 0)
        return null;

    return [id, monitorIndex, panelPosition];
};

/**
 * toStandardIconSize:
 * @maxSize (integer): the maximum size of the icon
 *
 * Calculates the nearest standard icon size up to a maximum.
 *
 * Returns: an integer, the icon size
 */
function toStandardIconSize(maxSize) {
    maxSize = Math.floor(maxSize);
    if (maxSize < 22) return 16;
    else if (maxSize < 24) return 22;
    else if (maxSize < 32) return 24;
    else if (maxSize < 48) return 32;
    // Panel icons reach 32 at most with the largest panel, also on hidpi
    return 48;
}

function setHeightForPanel(panel) {
    let height;

    // for vertical panels use the width instead of the height
    if (panel.panelPosition > 1)
        height = panel.get_width();
    else
        height = panel.get_height();

    if (height < 20)
        height = 40;

    return height;
}

function getPanelsEnabledList() {
    return global.settings.get_strv("panels-enabled");
}

function setPanelsEnabledList(list) {
    global.settings.set_strv("panels-enabled", list);
}

function updatePanelsMeta(meta, panel_props) {
    let changed = false;

    // panelMeta is [panel_id][monitor, position]
    // Find matching panel ids and update their monitor value if necessary.
    for (let i = 0; i < meta.length; i++) {
        if (!meta[i])
            continue;

        panel_props.forEach(prop => {
            let elements = prop.split(":");
            let [id_str, lmon_str, pos_str] = elements;

            let id = parseInt(id_str);

            if (id === i) {
                let l_mon = parseInt(lmon_str);

                if (meta[i][0] !== l_mon) {
                    meta[i][0] = l_mon;
                    changed = true;
                }
            }
        });
    }

    return changed;
}

/**
 * #PanelManager
 *
 * @short_description: Manager of Cinnamon panels
 *
 * #PanelManager creates panels and startup and
 * provides methods for easier access of panels
 */
var PanelManager = GObject.registerClass({
    Signals: {
        'monitors-changed': {},
    },
}, class PanelManager extends GObject.Object {
    _init() {
        super._init();

        this.dummyPanels = [];
        this.panelCount = 0;
        this.panels = [];
        // Properties of panels in format [<monitor index>, <panelPosition>]
        this.panelsMeta = [];
        this.canAddPanel = true;
        this.monitorCount = global.display.get_n_monitors();

        // don't start up in edit mode, can loop with empty vertical panels
        let editMode = global.settings.get_boolean("panel-edit-mode");
        if (editMode == true)
            global.settings.set_boolean("panel-edit-mode", false);

        this._fullPanelLoad();
        this._setMainPanel();

        this.addPanelMode = false;
        this.handlingPanelsChanged = false;

        global.settings.connect("changed::panels-enabled", this._onPanelsEnabledChanged.bind(this));
        global.settings.connect("changed::panel-edit-mode", this._onPanelEditModeChanged.bind(this));
        Main.layoutManager.connect("monitors-changed", this._onMonitorsChanged.bind(this));

        this._addOsd = new ModalDialog.InfoOSD(_("Select position of new panel. Esc to cancel."));
        this._moveOsd = new ModalDialog.InfoOSD(_("Select new position of panel. Esc to cancel."));
        this._addOsd.hide();
        this._moveOsd.hide();

        this._checkCanAddPanel();
        this._updateAllPointerBarriers();
    }

    /**
     * #_fullPanelLoad
     *
     * @short_description: Does a full load of all panels
     *
     * #_fullPanelLoad loads all panels in order, and makes any adjustments to permit
     *                 vertical panels to fit snugly between horizontal ones
     */
    _fullPanelLoad() {
        // panel id, monitor, panel type
        let stash = [];

        let panelDefs = getPanelsEnabledList();

        // First pass through to discard unusable and conflicting definitions. There is no
        // ordering to rely on, so of a conflicting pair whichever comes first is kept.
        let keptDefs = [];
        let removals = [];
        let seenIds = new Set();
        let seenSlots = new Set();

        for (let i = 0, len = panelDefs.length; i < len; i++) {
            let def = parsePanelDef(panelDefs[i]);
            if (!def) {
                global.log("Invalid panel definition: " + panelDefs[i]);
                removals.push(panelDefs[i]);
                continue;
            }

            let [id, monitorIndex, panelPosition] = def;
            let slot = monitorIndex + ":" + panelPosition;
            let duplicate = false;

            if (seenIds.has(id)) {
                global.log("Duplicate ID detected in panel definition: " + panelDefs[i]);
                duplicate = true;
            } else if (seenSlots.has(slot)) {
                global.log("Duplicate monitor+position detected in panel definition: " + panelDefs[i]);
                duplicate = true;
            }

            if (duplicate) {
                // A missing monitor can be a temporary condition, so leave its definitions in
                // settings and let a session that can see the monitor resolve the conflict.
                if (monitorIndex < this.monitorCount)
                    removals.push(panelDefs[i]);
                else
                    keptDefs.push(panelDefs[i]);
                continue;
            }

            seenIds.add(id);
            seenSlots.add(slot);
            keptDefs.push(panelDefs[i]);
            stash.push(def);
        }

        // Definitions we merely failed to understand are the user's entire panel layout, so
        // don't write an empty list over all of them - there would be nothing to recover from.
        if (removals.length > 0 && keptDefs.length > 0) {
            global.log("Removing invalid panel definitions: " + removals);
            setPanelsEnabledList(keptDefs);
        }

        // Panels belonging to monitors that aren't currently connected can't be created, but
        // _loadPanel() still records their metadata. Without it _onMonitorsChanged() has no way
        // of knowing they exist, and can't restore them when the monitor is plugged back in.
        for (let i = 0, len = stash.length; i < len; i++) {
            let [id, monitorIndex, panelPosition] = stash[i];
            if (monitorIndex < this.monitorCount)
                continue;

            this._loadPanel(id, monitorIndex, panelPosition);

            // _loadPanel() returns null here either way, so check the metadata itself - it
            // can reject a definition before recording it, leaving nothing to restore from.
            if (!this.panelsMeta[id])
                global.logError("Panel " + id + " could not be registered, and will not be " +
                                "restored if monitor " + monitorIndex + " is reconnected");
        }

        // When using mixed horizontal and vertical panels draw the vertical panels first.
        // This is done so that when using a box shadow on the panel to create a border the border will be drawn over the
        // top of the vertical panel.
        for (let i = 0; i < this.monitorCount; i++) {
            let pleft, pright;
            for (let j = 0, len = stash.length; j < len; j++) {
                if (stash[j][2] == PanelLoc.left && stash[j][1] == i) {
                    pleft = this._loadPanel(stash[j][0], stash[j][1], stash[j][2]);
                }
                if (stash[j][2] == PanelLoc.right && stash[j][1] == i) {
                    pright = this._loadPanel(stash[j][0], stash[j][1], stash[j][2]);
                }
                if (stash[j][2] == PanelLoc.bottom && stash[j][1] == i) {
                    this._loadPanel(stash[j][0], stash[j][1], stash[j][2]);
                }
                if (stash[j][2] == PanelLoc.top && stash[j][1] == i) {
                    this._loadPanel(stash[j][0], stash[j][1], stash[j][2]);
                }
            }
            //
            // if called in init, the calls in moveResizePanel that happen when panels are created will not
            // have found the heights available for vertical panels between horizontal panels, so calculate them now.
            //
            if (pleft || pright) {
                let toppheight;
                let botpheight;
                [toppheight,botpheight] = heightsUsedMonitor(i, this.panels);
                if (pleft) {
                    pleft.toppanelHeight = toppheight;
                    pleft.bottompanelHeight = botpheight;;
                }
                if (pright) {
                    pright.toppanelHeight = toppheight;
                    pright.bottompanelHeight = botpheight;
                }
            }
        }

        this._adjustVerticalPanelHeights();
    }

   /**
     * disablePanels:
     *
     * Disables (hide and lock) all panels
     */
    disablePanels() {
        for (let i = 0, len = this.panels.length; i < len; i++) {
            if (this.panels[i])
                this.panels[i].disable();
        }
    }

    /**
     * enablePanels:
     *
     * Enables all panels
     */
    enablePanels() {
        for (let i = 0, len = this.panels.length; i < len; i++) {
            if (this.panels[i])
                this.panels[i].enable();
        }
    }

    /**
     * setPanelsOpacity:
     * @opacity (int): opacity of panels
     *
     * Sets the opacity of all panels to @opacity
     */
    setPanelsOpacity(opacity) {
        for (let i = 0, len = this.panels.length; i < len; i++) {
            if (this.panels[i])
                this.panels[i].actor.opacity = opacity;
        }
    }

    /**
     * lowerActorBelowPanels:
     * @actor (ClutterActor): actor to stack below the panels
     *
     * Lowers actor to just under the panel actors
     */
    lowerActorBelowPanels(actor, group) {
        for (let i = 0, len = this.panels.length; i < len; i++) {
            if (!this.panels[i])
                continue;
            Main.uiGroup.set_child_below_sibling(actor, this.panels[i].actor);
            break;
        }

        let prev = actor.get_previous_sibling();

        while (true) {
            if (prev && prev._delegate && prev._delegate instanceof Panel) {
                Main.uiGroup.set_child_below_sibling(actor, prev);
                prev = actor.get_previous_sibling();
                continue;
            } else {
                break;
            }
        }
    }

    /**
     * removePanel:
     * @panelId (int): Panel id of the panel to be removed
     *
     * Remove the panel from the list panels-enabled
     */
    removePanel(panelId) {
        this.panelCount -= 1;
        let list = getPanelsEnabledList();
        for (let i = 0, len = list.length; i < len; i++) {
            if (list[i].split(":")[0] == panelId) {
                list.splice(i, 1);
                break;
            }
        }

        setPanelsEnabledList(list);
    }

    /**
     * addPanel:
     * @monitorIndex (integer): monitor to be added to
     * @panelPosition (integer): where the panel is added
     *
     * Adds a new panel to the specified position
     */
    addPanel(monitorIndex, panelPosition) {
        let list = getPanelsEnabledList();
        let i = 0; // Start counting at 1 for compatibility

        // Magic: Keep recursing until there is a free panel id
        while (true)
            if (!this.panelsMeta[++i])
                break;

        // Add default values
        outerLoop:
        for (let key in DEFAULT_PANEL_VALUES) {
            let settings = global.settings.get_strv(key);
            for (let j = 0; j < settings.length; j++) {
                if (settings[j].split(":")[0] == i) {
                    continue outerLoop;
                }
            }
            settings.push(i + ":" + DEFAULT_PANEL_VALUES[key]);
            global.settings.set_strv(key, settings);
        }

        switch (panelPosition)
        {
            case PanelLoc.top:
                list.push(i + ":" + monitorIndex + ":" + "top");
                break;
            case PanelLoc.bottom:
                list.push(i + ":" + monitorIndex + ":" + "bottom");
                break;
            case PanelLoc.left:
                list.push(i + ":" + monitorIndex + ":" + "left");
                break;
            case PanelLoc.right:
                list.push(i + ":" + monitorIndex + ":" + "right");
                break;
            default:
                global.log("addPanel - unrecognised panel position " + panelPosition);
        }
        setPanelsEnabledList(list);

        if (this.addPanelMode)
            this._destroyDummyPanels();
    }

    /**
     * movePanel:
     * @monitorIndex (integer): monitor to be added to
     * @panelPosition (integer): where the panel is added
     *
     * Moves the panel of id this.moveId to the specified position
     */
    movePanel(monitorIndex, panelPosition) {
        let list = getPanelsEnabledList();
        let i = -1;

        for (let i = 0, len = list.length; i < len; i++) {
            if (list[i].split(":")[0] == this.moveId) {
                switch (panelPosition)
                {
                    case PanelLoc.top:
                        list[i] = this.moveId + ":" + monitorIndex + ":" + "top";
                        break;
                    case PanelLoc.bottom:
                        list[i] = this.moveId + ":" + monitorIndex + ":" + "bottom";
                        break;
                    case PanelLoc.left:
                        list[i] = this.moveId + ":" + monitorIndex + ":" + "left";
                        break;
                    case PanelLoc.right:
                        list[i] = this.moveId + ":" + monitorIndex + ":" + "right";
                        break;
                    default:
                    global.log("movePanel - unrecognised panel position "+panelPosition);
                }
            break;
            }
        }

        setPanelsEnabledList(list);

        if (this.addPanelMode)
            this._destroyDummyPanels();
    }

    /**
     * _destroyDummyPanels:
     *
     * Destroys all panel dummies
     */
    _destroyDummyPanels() {
        for (let i = 0, len = this.dummyPanels.length; i < len; i++) {
            let removedDummyPanelIndexes = [];
            for (let j = 0, len = this.dummyPanels[i].length; j < len; j++) {
                if (this.dummyPanels[i][j]) {
                    this.dummyPanels[i][j].destroy();
                    removedDummyPanelIndexes.push(j);
                }
            }
            for (let z = 0; z < removedDummyPanelIndexes.length; z++) {
                this.dummyPanels[i][removedDummyPanelIndexes[z]] = undefined;
                this.dummyPanels[i].splice(removedDummyPanelIndexes[z], 1);
            }
        }
        this.addPanelMode = false;
        this._addOsd.hide();
        this._moveOsd.hide();
        Main.keybindingManager.removeHotKey('close-add-panel');
    }

    /**
     * getPanelsInMonitor:
     * @monitorIndex (integer): index of monitor
     *
     * Retrieves all the panels in the monitor of index @monitorIndex
     *
     * Returns: an array of panels
     */
    getPanelsInMonitor(monitorIndex) {
        let returnValue = [];
        for (let i = 0, len = this.panels.length; i < len; i++) {
            if (this.panels[i] && this.panels[i].monitorIndex == monitorIndex)
                returnValue.push(this.panels[i]);
        }
        return returnValue;
    }

    /**
     * getPanels:
     *
     * Retrieves all panels
     *
     * Returns: an array of panels
     */

    getPanels() {
        return this.panels;
    }

    /**
     * getPanelForActor:
     * @actor (Clutter.Actor): an actor
     *
     * Finds the panel containing @actor
     *
     * Returns: the panel containing @actor (null if no panel does)
     */
    getPanelForActor(actor) {
        return this.panels.find(panel => panel && panel.contains(actor)) || null;
    }

    /**
     * getPanel:
     * @monitorIndex (integer): index of monitor
     * @panelPosition (integer): where the panel is added
     *
     * Gets a specific panel in monitor @monitorIndex
     *
     * Returns: the panel required (null if panel not found)
     */
    getPanel(monitorIndex, panelPosition) {
        for (let i = 0, len = this.panels.length; i < len; i++) {
            if (!this.panels[i])
                continue;
            if (this.panels[i].monitorIndex == monitorIndex && this.panels[i].panelPosition == panelPosition)
                return this.panels[i];
        }
        return null;
    }

    /**
     * updatePanelsVisibility:
     *
     * Prompts every panel to update its visibility (show/hide). This is used
     * by WindowManager after window map/tile/etc animations, and after popup
     * menus close.
     */
    updatePanelsVisibility() {
        for (let i = 0, len = this.panels.length; i < len; i++) {
             if (!this.panels[i])
                 continue;
             this.panels[i]._updatePanelVisibility();
        }
    }

    /**
     * _loadPanel:
     * @ID (integer): panel id
     * @monitorIndex (integer): index of monitor of panel
     * @panelPosition (integer): where the panel should be
     * @panelList (array): (optional) the list in which the new panel should be appended to (not necessarily this.panels, c.f. _onPanelsEnabledChanged) Default: this.panels
     * @metaList(array): (optional) the list in which the new panel metadata should be appended to (not necessarily this.panelsMeta, c.f. _onPanelsEnabledChanged)
     *                   Default: this.panelsMeta
     *
     * Loads a panel with the given properties and appends it to @panelList. @panelList is usually this.panels but is a different array when used by _onPanelsEnabledChanged.
     *
     * Returns (Panel.Panel): Panel created
     */
    _loadPanel(ID, monitorIndex, panelPosition, panelList, metaList) {
        if (!panelList)
            panelList = this.panels;
        if (!metaList)
            metaList = this.panelsMeta;

        if (panelList[ID]) {
            global.log("Multiple panels with same ID (" + ID + ") are found");
            return null;
        }

        panelList.length = Math.max(panelList.length, ID + 1);
        metaList.length = panelList.length;

        let repeat = false;
        for (let i = 0, len = metaList.length; i < len; i++) {
            if (!metaList[i]) {
                continue;
            }
            if ((metaList[i][0] == monitorIndex) && (metaList[i][1] == panelPosition) && i != ID) {
                switch (panelPosition)
                {
                    case PanelLoc.top:
                        global.log("Conflicting panel definitions: " + ID + ":" + monitorIndex + ":" + "top" );
                        break;
                    case PanelLoc.bottom:
                        global.log("Conflicting panel definitions: " + ID + ":" + monitorIndex + ":" + "bottom" );
                    break;
                    case PanelLoc.left:
                        global.log("Conflicting panel definitions: " + ID + ":" + monitorIndex + ":" + "left" );
                    break;
                    case PanelLoc.right:
                        global.log("Conflicting panel definitions: " + ID + ":" + monitorIndex + ":" + "right" );
                    break;
                    default:
                    global.log("loadPanel - unrecognised panel position "+panelPosition);
                }
                repeat = true;
                break;
            }
        }

        if (repeat)
            return null;

        // metaList [i][0] is the monitor index, metaList [i][1] is the panelPosition
        metaList[ID] = [monitorIndex, panelPosition];

        // Keep this check below the metadata assignment - a panel whose monitor index is out of
        // range is still registered here, which is what lets _onMonitorsChanged() restore it
        // once that monitor is connected.
        if (monitorIndex < 0 || monitorIndex >= this.monitorCount) {
            global.log("Not creating panel " + ID + ", monitor " + monitorIndex + " not found");
            return null;
        }
        let[toppheight,botpheight] = heightsUsedMonitor(monitorIndex, panelList);
        panelList[ID] = new Panel(ID, monitorIndex, panelPosition, toppheight, botpheight);
        this.panelCount += 1;

        return panelList[ID];
    }

    _checkCanAddPanel() {
        let panelCount = (this.monitorCount * 4) - this.panelCount;
        this.canAddPanel = panelCount > 0;
    }

    _updateAllPointerBarriers() {
        for (let i = 0, len = this.panels.length; i < len; i++) {
            if (this.panels[i])
                this.panels[i]._updatePanelBarriers();
        }
    }

    /**
     * _adjustVerticalPanelHeights:
     *
     * Re-fits every vertical panel to the horizontal panels currently on its monitor. Call
     * this after a batch of panel creation or destruction - a vertical panel built before
     * the horizontal panels it has to fit between sized itself against the wrong heights.
     */
    _adjustVerticalPanelHeights() {
        for (let i = 0, len = this.panels.length; i < len; i++) {
            if (!this.panels[i])
                continue;
            if (this.panels[i].panelPosition == PanelLoc.left || this.panels[i].panelPosition == PanelLoc.right)
                this.panels[i]._moveResizePanel();
        }
    }

    /**
     * _onPanelsEnabledChanged:
     *
     * This will be called whenever the panels-enabled settings key is changed
     * i.e. when panels are added, moved or removed.
     */
    _onPanelsEnabledChanged() {
        if (this.handlingPanelsChanged)
            return;

        this.handlingPanelsChanged = true;

        let newPanels = new Array(this.panels.length);
        let newMeta = new Array(this.panels.length);
        let panelProperties = getPanelsEnabledList();

        for (let i = 0; i < panelProperties.length; i ++) {

            let def = parsePanelDef(panelProperties[i]);
            if (!def) {
                global.log("Invalid panel definition: " + panelProperties[i]);
                continue;
            }

            let [ID, mon, ploc] = def;

            // If (existing) panel is moved
            if (this.panels[ID]) {
                newMeta[ID] = [mon, ploc];

                newPanels[ID] = this.panels[ID];
                this.panels[ID] = null; // avoids triggering the destroy logic that follows
                delete this.panels[ID];

                if (newMeta[ID][0] != this.panelsMeta[ID][0] ||
                    newMeta[ID][1] != this.panelsMeta[ID][1]) {
                    newPanels[ID].updatePosition(newMeta[ID][0], newMeta[ID][1]);

                    // Asymmetrical applets such as panel launchers, systray etc.
                    // need reorienting within the applet using their
                    // on_orientation_changed function
                    AppletManager.updateAppletsOnPanel(newPanels[ID]);
                }
            } else {  // new panel
                let panel = this._loadPanel(ID,
                                            mon,
                                            ploc,
                                            newPanels,
                                            newMeta);
                if (panel)
                     AppletManager.loadAppletsOnPanel(panel);
            }
        }

        // Destroy removed panels
        let removedPanelIndexes = [];
        for (let i = 0, len = this.panels.length; i < len; i++) {
            if (this.panels[i]) {
                this.panels[i].destroy();
                removedPanelIndexes.push(i);
            }
        }
        for (let i = 0, len = removedPanelIndexes.length; i < len; i++) {
            this.panels[i] = undefined;
            this.panels.splice(removedPanelIndexes[i], 1);
        }

        this.panels = newPanels;
        this.panelsMeta = newMeta;

        // Scope for minor optimisation here - only needed if a horizontal panel was added or
        // removed, or if space made for panel dummys has to be reflected.
        this._adjustVerticalPanelHeights();

        this._setMainPanel();
        this._checkCanAddPanel();
        this._updateAllPointerBarriers();

        // If the user removed the last panel, pop up a dialog to ask if they want to open panel settings
        if (panelProperties.length == 0) {
            let lastPanelRemovedDialog = new ModalDialog.ConfirmDialog(
                _("You don't have any panels added.\nDo you want to open panel settings?"),
                () => { Util.spawnCommandLine("cinnamon-settings panel"); });
            lastPanelRemovedDialog.open();
        }

        this.handlingPanelsChanged = false;
    }

    _onMonitorsChanged() {
        const oldCount = this.monitorCount;
        this.monitorCount = global.display.get_n_monitors();

        let panelProperties = getPanelsEnabledList()
        // adjust any changes to logical/xinerama monitor relationships
        let monitors_changed = updatePanelsMeta(this.panelsMeta, panelProperties) || oldCount !== this.monitorCount;
        let panelsRestored = false;

        for (let i = 0, len = this.panelsMeta.length; i < len; i++) {
            if (!this.panelsMeta[i])
                continue;
            // If there is a meta but not a panel, i.e. panel could not create due to non-existent monitor, try again
            // the monitor may have just been reconnected
            if (!this.panels[i]) {
                if (this.panelsMeta[i][0] < this.monitorCount) {
                    let panel = this._loadPanel(i, this.panelsMeta[i][0], this.panelsMeta[i][1]);
                    if (panel) {
                        AppletManager.loadAppletsOnPanel(panel);
                        panelsRestored = true;
                    }
                }
            } else if (this.panelsMeta[i][0] >= this.monitorCount) {
                if (this.panels[i]) {
                    // destroy panel, but don't remove icon size settings
                    this.panels[i].destroy(false);
                    delete this.panels[i];
                    this.panelCount -= 1;
                }

            } else {
                this.panels[i]._monitorsChanged = true;

                if (monitors_changed) {
                    this.panels[i].updatePosition(...this.panelsMeta[i]);
                    this.emit("monitors-changed");
                } else {
                     // Nothing happens. Re-allocate panel
                    this.panels[i]._moveResizePanel();
                }
            }
        }

        // Restored panels are created in id order, so a vertical panel may have been built before
        // the horizontal panels it has to fit between.
        if (panelsRestored)
            this._adjustVerticalPanelHeights();

        if (this.addPanelMode) {
            this._destroyDummyPanels();
            this._showDummyPanels(this.dummyCallback);
        }

        this._setMainPanel();
        this._checkCanAddPanel();
        this._updateAllPointerBarriers();
    }

    _onPanelEditModeChanged() {
        if (!global.settings.get_boolean("panel-edit-mode")) {
            if (this.addPanelMode)
                this._destroyDummyPanels();
        }
    }

    /**
     * addPanelQuery:
     *
     * Prompts user where to add the panel
     */
    addPanelQuery() {
        if (this.addPanelMode || !this.canAddPanel)
            return;

        this._showDummyPanels(this.addPanel.bind(this));
        this._addOsd.show();
    }

    /**
     * movePanelQuery:
     * @id (integer): the id of the panel to be moved
     *
     * Prompts user where to move the panel
     */
    movePanelQuery(id) {
        if (this.addPanelMode || !this.canAddPanel)
            return;

        this.moveId = id;
        this._showDummyPanels(this.movePanel.bind(this));
        this._moveOsd.show();
    }

    /**
     * _showDummyPanels:
     * @callback (): callback
     *
     * shows the dummy panels
     */
    _showDummyPanels(callback) {
        this.dummyCallback = callback;
        this.dummyPanels = [];

        while (this.dummyPanels.push([true, true, true, true]) < this.monitorCount);

        for (let i = 0, len = this.panelsMeta.length; i < len; i++) {
            if (!this.panelsMeta[i]) {
                continue;
            }
            if (this.panelsMeta[i][0] >= this.monitorCount)
                continue;
            // there is an existing panel showing
            this.dummyPanels[this.panelsMeta[i][0]][this.panelsMeta[i][1]] = false;
        }

        for (let i = 0; i < this.monitorCount; i++) {
            for (let j = 0; j < 4; j++) {
                if (this.dummyPanels[i] && this.dummyPanels[i][j] == true) { // no panel there at the moment, so show a dummy
                    this.dummyPanels[i][j] = new PanelDummy(i, j, callback);
                }
            }
        }

        this.addPanelMode = true;
        Main.keybindingManager.addHotKey('close-add-panel', 'Escape', () => {
            if (this.addPanelMode)
                this._destroyDummyPanels();
        });

       return true;
    }

    // Set Main.panel so that applets that look for it don't break
    _setMainPanel() {
        for (let i = 0; i < this.panels.length; i++) {
            if (this.panels[i]) {
                Main.panel = this.panels[i];
                break;
            }
        }
    }

    resetPanelDND() {
        for (let i = 0; i < this.panels.length; i++) {
            if (this.panels[i]) {
                this.panels[i].resetDNDZones();
            }
        }
    }

});

/**
 * #PanelDummy
 * @short_description: Dummy panels for users to select new position of panel
 *
 * #PanelDummy creates some boxes at possible panel locations for users to
 * select where to place their new panels
 */
var PanelDummy = GObject.registerClass(
class PanelDummy extends St.Widget {
    _init(monitorIndex, panelPosition, callback) {
        super._init({
            style_class: 'panel-dummy',
            reactive: true,
            track_hover: true,
            important: true,
        });

        this.monitorIndex = monitorIndex;
        this.panelPosition = panelPosition;
        this.callback = callback;
        this.monitor = global.display.get_monitor_geometry(monitorIndex);
        const defaultheight = 40 * global.ui_scale;

        Main.layoutManager.addChrome(this, { addToWindowgroup: false });

        // layouts set to be full width horizontal panels,
        // and vertical panels set to use as much available space as is left
        let tpanelHeight = 0;
        let bpanelHeight = 0;

        if (Main.panelManager) {
            [tpanelHeight, bpanelHeight] = heightsUsedMonitor(this.monitorIndex, Main.panelManager.panels);
        } else {
            tpanelHeight = defaultheight;
            bpanelHeight = defaultheight;
        }

        switch (panelPosition) {
            case PanelLoc.top:
                this.set_size(this.monitor.width, defaultheight);
                this.set_position(this.monitor.x,  this.monitor.y);
                break;
            case PanelLoc.bottom:
                this.set_size(this.monitor.width, defaultheight);
                this.set_position(this.monitor.x, this.monitor.y + this.monitor.height - defaultheight);
                break;
            case PanelLoc.left:
                this.set_size( defaultheight,this.monitor.height - tpanelHeight - bpanelHeight);
                this.set_position(this.monitor.x,  this.monitor.y + tpanelHeight);
                break;
            case PanelLoc.right:
                this.set_size( defaultheight,this.monitor.height - tpanelHeight - bpanelHeight);
                this.set_position(this.monitor.x + this.monitor.width - defaultheight, this.monitor.y + tpanelHeight);
                break;
            default:
                global.log("paneDummy - unrecognised panel position " + panelPosition);
        }
    }

    vfunc_button_press_event(event) {
        this.callback(this.monitorIndex, this.panelPosition);
        return Clutter.EVENT_STOP;
    }

    vfunc_enter_event(event) {
        this.add_style_pseudo_class('entered');
        if (this.noStyle)
            this.opacity = 160;
        return Clutter.EVENT_STOP;
    }

    vfunc_leave_event(event) {
        this.remove_style_pseudo_class('entered');
        if (this.noStyle)
            this.opacity = 100;
        return Clutter.EVENT_STOP;
    }

    /**
     * destroy:
     *
     * Destroys panel dummy actor
     */
    destroy() {
        Main.layoutManager.removeChrome(this);

        super.destroy();
    }
});

var SettingsLauncher = class SettingsLauncher extends PopupMenu.PopupIconMenuItem {
    _init (label, keyword, icon) {
        super._init.call(this, label, icon, St.IconType.SYMBOLIC);

        this._keyword = keyword;
        this.connect('activate', () => {
            Util.spawnCommandLine("cinnamon-settings " + this._keyword);
        });
    }
}

var PanelContextMenu = class PanelContextMenu extends PopupMenu.PopupMenu {
    _init(launcher, orientation, panelId) {
        super._init.call(this, launcher.actor, orientation);

        Main.uiGroup.add_actor(this.actor);
        this.actor.hide();
        this.panelId = panelId;

        const moreSettingsMenuItem = new SettingsLauncher(
            _("Panel settings"), "panel --panel " + panelId, "xsi-cog");
        this.addMenuItem(moreSettingsMenuItem);

        const applet_settings_item = new SettingsLauncher(
            _("Applets"), "applets --panel " + panelId, "xsi-addon");
        this.addMenuItem(applet_settings_item);

        let menu = this;

        menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        // Panel Edit mode
        let editMode = global.settings.get_boolean("panel-edit-mode");
        let panelEditMode = new PopupMenu.PopupSwitchMenuItem(_("Panel edit mode"), editMode);
        panelEditMode.connect('toggled', (item) => {
            global.settings.set_boolean("panel-edit-mode", item.state);
        });
        menu.addMenuItem(panelEditMode);
        this._panelEditSettingId = global.settings.connect('changed::panel-edit-mode', () => {
            panelEditMode.setToggleState(global.settings.get_boolean("panel-edit-mode"));
        });

        this.connect("destroy", () => {
            global.settings.disconnect(this._panelEditSettingId);
            this._panelEditSettingId = 0;
        });

        menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        menu.movePanelItem = new PopupMenu.PopupIconMenuItem(_("Move"), "xsi-move", St.IconType.SYMBOLIC);
        menu.movePanelItem.activate = () => {
            Main.panelManager.movePanelQuery(this.panelId);
            this.close(true);
        };
        menu.addMenuItem(menu.movePanelItem);

        let menuItem = new PopupMenu.PopupIconMenuItem(_("Remove"), "xsi-list-remove", St.IconType.SYMBOLIC);
        menuItem.activate = () => {
            let confirm = new ModalDialog.ConfirmDialog(_("Are you sure you want to remove this panel?"),
                    () => {
                        Main.panelManager.removePanel(panelId);
                    });
            confirm.open();
        };
        menu.addMenuItem(menuItem);

        menu.addPanelItem = new PopupMenu.PopupIconMenuItem(_("Add a new panel"), "xsi-list-add", St.IconType.SYMBOLIC);
        menu.addPanelItem.activate = () => {
            Main.panelManager.addPanelQuery();
            this.close(true);
        };
        menu.addMenuItem(menu.addPanelItem);

        menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        menu.troubleshootItem = new PopupMenu.PopupSubMenuMenuItem(_("Troubleshoot"));
        menu.troubleshootItem.menu.addAction(_("Restart Cinnamon"), (event) => {
            Main.restartCinnamon(true);
        });

        menu.troubleshootItem.menu.addAction(_("Looking Glass"), (event) => {
            Main.createLookingGlass().open();
        });

        menu.troubleshootItem.menu.addAction(_("Restore all settings to default"), (event) => {
            let confirm = new ModalDialog.ConfirmDialog(_("Are you sure you want to restore all settings to default?\n\n"),
                    () => {
                        Util.spawnCommandLine("gsettings reset-recursively org.cinnamon");
                        Util.spawnCommandLine("gsettings reset-recursively org.cinnamon.desktop.input-sources");
                        Main.restartCinnamon(true);
                    });
            confirm.open();
        });

        menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        menu.addMenuItem(menu.troubleshootItem);

        this.addMenuItem(new SettingsLauncher(_("System Settings"), "", "xsi-preferences"));
    }

    open(animate) {
        super.open.call(this, animate);

        this.movePanelItem.setSensitive(Main.panelManager.canAddPanel);
        this.addPanelItem.setSensitive(Main.panelManager.canAddPanel);

        let {definitions} = AppletManager;
        let nonEmpty = false;
        for (let i = 0, len = definitions.length; i < len; i++) {
            if (definitions[i] && definitions[i].panelId === this.panelId) {
                nonEmpty = true;
                break;
            }
        }
    }
};

var PanelZoneDNDHandler = class {
    constructor(panelZone, zoneString, panelId) {
        this._panelZone = panelZone;
        this._panelZone._delegate = this;
        this._zoneString = zoneString;
        this._panelId = panelId;
        this._dragPlaceholder = null;
        this._dragPlaceholderPos = -1;

        this._origAppletCenters = null;
        this._origAppletPos = -1;

        this._panelZone.connect('leave-event', this._handleLeaveEvent.bind(this));
    }

    handleDragOver(source, actor, x, y, time) {
        if (!(source instanceof Applet.Applet))
            return DND.DragMotionResult.NO_DROP;

        if (!this._hasSupportedLayout(source))
            return DND.DragMotionResult.NO_DROP;

        let vertical_panel = this._panelZone.get_parent()._delegate.is_vertical;
        let children = this._panelZone.get_children();
        let horizontal_rtl = St.Widget.get_default_direction () === St.TextDirection.RTL && !vertical_panel;

        if (this._origAppletCenters == null) {
            this._origAppletCenters = [];
            this._origAppletPos = children.indexOf(source.actor);

            let j;

            for (j = 0; j < children.length; j++) {
                let allocation = children[j].get_allocation_box();
                let center = 0;
                if (vertical_panel)
                    center = (allocation.y1 + allocation.y2) / 2;
                else
                    center = (allocation.x1 + allocation.x2) / 2;

                this._origAppletCenters.push(center);
            }

            if(horizontal_rtl)
                this._origAppletCenters.reverse();
        }

        let dragPos = vertical_panel ? y : x;
        let pos = 0, i = 0;

        while (i < this._origAppletCenters.length && dragPos > this._origAppletCenters[i]) {
            i++;
        }

        if(horizontal_rtl)
            pos = this._origAppletCenters.length - i;
        else
            pos = i;

        if (pos != this._dragPlaceholderPos) {
            this._dragPlaceholderPos = pos;

            // Don't allow positioning before or after self
            if (this._origAppletPos != -1 && (pos == this._origAppletPos || pos == this._origAppletPos + 1)) {
                this._clearDragPlaceholder();
                return DND.DragMotionResult.CONTINUE;
            }

            // If the placeholder already exists, we just move
            // it, but if we are adding it, expand its size in
            // an animation
            if (this._dragPlaceholder) {
                this._panelZone.set_child_at_index(this._dragPlaceholder,
                                                   this._dragPlaceholderPos);
            } else {
                this._dragPlaceholder = new DND.GenericDragPlaceholderItem();

                if (vertical_panel) {
                    this._dragPlaceholder.set_width (10 * global.ui_scale);
                    this._dragPlaceholder.set_height (20 * global.ui_scale);
                } else {
                    this._dragPlaceholder.set_width (20 * global.ui_scale);
                    this._dragPlaceholder.set_height (10 * global.ui_scale);
                }

                this._panelZone.insert_child_at_index(this._dragPlaceholder,
                                                      this._dragPlaceholderPos);

                this._dragPlaceholder.animateIn();
            }
        }

        return DND.DragMotionResult.MOVE_DROP;
    }

    _handleLeaveEvent() {
        this._clearDragPlaceholder();
    }

    handleDragOut() {
        this._clearDragPlaceholder();
    }

    acceptDrop(source, actor, x, y, time) {
        this._origAppletCenters = null;

        if (!(source instanceof Applet.Applet))
            return false;

        //  We want to ensure that applets placed in a panel can be shown correctly
        //  If the applet is of type Icon Applet then should be fine
        //  otherwise we look to see if it has declared itself suitable
        if (source instanceof Applet.TextIconApplet || !(source instanceof Applet.IconApplet)) {
            if (!this._hasSupportedLayout(source))
                    return false;
        }

        let children = this._panelZone.get_children();
        let curAppletPos = 0;
        let insertAppletPos = -1;

        const {
            panel: { panelId: sourceAppletPanel },
            locationLabel: sourceAppletLocation,
        } = source.actor._applet;

        for (let i = 0, len = children.length; i < len; i++) {
            if (children[i]._delegate instanceof Applet.Applet) {
                children[i]._applet._newOrder = curAppletPos;
                curAppletPos++;
            } else if (children[i] == this._dragPlaceholder) {
                insertAppletPos = curAppletPos;
                curAppletPos++;
            }
        }

        const isSameLocation = (
            sourceAppletPanel === this._panelId
            && sourceAppletLocation === this._zoneString
            && insertAppletPos === -1
        );
        if (!isSameLocation)
            source.actor._applet._newOrder = insertAppletPos === -1 ? 0 : insertAppletPos;
        source.actor._applet._newPanelLocation = this._panelZone;
        source.actor._applet._zoneString = this._zoneString;
        source.actor._applet._newPanelId = this._panelId;

        // this is the panel box providing the applet
        let sourcebox = source.actor._applet._panelLocation;

        this._clearDragPlaceholder();
        AppletManager.saveAppletsPositions();

        /* this._panelZone is the panel box being dropped into. Note that the style class name will
           be something like 'panelLeft' or 'panelLeft vertical'*/

        if (this._panelZone.has_style_class_name("panelRight") || this._panelZone.has_style_class_name("panelLeft")) {
            this._panelZone.set_size(-1, -1);  /* kludge pt 2 - if the box being dropped into
                                                  has been set a fixed size then we need to let it adjust. */

        }

        if (sourcebox.has_style_class_name("panelRight") || sourcebox.has_style_class_name("panelLeft")) {
            children = sourcebox.get_children();

            if (children.length == 0) {         /* put back some minimum space if the source box is now empty */
                if (sourcebox.get_parent()._delegate.is_vertical) {
                    let height = sourcebox.get_height();
                    if (height < EDIT_MODE_MIN_BOX_SIZE * global.ui_scale)
                        sourcebox.set_height(EDIT_MODE_MIN_BOX_SIZE * global.ui_scale);
                } else {
                    let width = sourcebox.get_width();
                    if (width < EDIT_MODE_MIN_BOX_SIZE * global.ui_scale)
                        sourcebox.set_width(EDIT_MODE_MIN_BOX_SIZE * global.ui_scale);
                }
            }
        }

        return true;
    }

    _clearDragPlaceholder() {
        if (this._dragPlaceholder) {
            this._dragPlaceholder.animateOutAndDestroy();
            this._dragPlaceholder = null;
            this._dragPlaceholderPos = -1;
        }
    }

    _hasSupportedLayout(applet) {
        let layout = applet.getAllowedLayout();
        if (layout == Applet.AllowedLayout.BOTH)
            return true;

        if (applet instanceof Applet.IconApplet && !(applet instanceof Applet.TextIconApplet))
            return true;

        if (layout == ((this._panelZone.get_parent()._delegate.is_vertical) ? Applet.AllowedLayout.VERTICAL : Applet.AllowedLayout.HORIZONTAL))
            return true;

        return false;
    }

    reset() {
        this._origAppletCenters = null;
        this._origAppletPos = -1;
        this._clearDragPlaceholder();
    }
};

/**
 * #Panel:
 * @short_description: A panel object on the monitor
 *
 * @panelId (int): the id of the panel
 * @monitorIndex (int): the index of the monitor containing the panel
 * @toppanelHeight (int): the height already taken on the screen by a top panel
 * @bottompanelHeight (int): the height already taken on the screen by a bottom panel
 *
 * @monitor (Meta.Rectangle): the geometry (bounding box) of the monitor
 * @panelPosition (integer): where the panel is on the screen
 *
 * @_leftBox (St.BoxLayout): the box containing all the applets in the left region
 * @_centerBox (St.BoxLayout): the box containing all the applets in the center region
 * @_rightBox (St.BoxLayout): the box containing all the applets in the right region
 * @_hidden (boolean): whether the panel is currently hidden
 * @_disabled (boolean): whether the panel is disabled
 * @_panelEditMode (boolean): whether the panel edit mode is on
 * @_context_menu (Panel.PanelContextMenu): the context menu of the panel
 *
 * This represents a panel on the screen.
 */
var Panel = GObject.registerClass({
    Signals: {
        'size-changed': { param_types: [GObject.TYPE_INT] },
        'icon-size-changed': {},
    },
}, class Panel extends St.Widget {
    _init(id, monitorIndex, panelPosition, toppanelHeight, bottompanelHeight) {
        super._init({
            name: 'panel',
            reactive: true,
        });
        // Keeping this to avoid breaking things
        this.actor = this;

        this.panelId = id;
        this.monitorIndex = monitorIndex;
        this.monitor = global.display.get_monitor_geometry(monitorIndex);
        this.panelPosition = panelPosition;
        this.toppanelHeight = toppanelHeight;
        this.bottompanelHeight = bottompanelHeight;

        this.is_vertical = (this.panelPosition == PanelLoc.left || this.panelPosition == PanelLoc.right);

        this._hidden = false;
        this._disabled = false;
        this._panelEditMode = false;
        this._highlighted = false;
        this._autohideSettings = null;
        this._destroyed = false;
        this._positionChanged = false;
        this._monitorsChanged = false;
        this._mouseEntered = null;
        this._signalManager = new SignalManager.SignalManager(null);
        this.heightForZones = 0;
        this.margin_top = 0;
        this.margin_bottom = 0;
        this.margin_left = 0;
        this.margin_right = 0;
        this._leftPanelBarrier = 0;
        this._rightPanelBarrier = 0;
        this._topPanelBarrier = 0;
        this._bottomPanelBarrier = 0;
        this._shadowBox = null;
        this._panelZoneSizes = this._createEmptyZoneSizes();
        this._peeking = false;

        this.themeSettings = new Gio.Settings({ schema_id: 'org.cinnamon.theme' });
        this.addPanelStyleClass(this.panelPosition);

        this.actor._delegate = this;

        this._menus = new PopupMenu.PopupMenuManager(this);

        this._leftBox = new St.BoxLayout({ name: 'panelLeft', style_class: 'panelLeft', important: true });
        this._rightBox = new St.BoxLayout({ name: 'panelRight', style_class: 'panelRight', important: true });
        this._centerBox = new St.BoxLayout({ name: 'panelCenter',  style_class: 'panelCenter', important: true });

        if (this.is_vertical)
            this._set_vertical_panel_style();
        else
            this._set_horizontal_panel_style();

        this.add_child(this._leftBox);
        this.add_child(this._centerBox);
        this.add_child(this._rightBox);

        this._leftBoxDNDHandler = new PanelZoneDNDHandler(this._leftBox, 'left', this.panelId);
        this._centerBoxDNDHandler = new PanelZoneDNDHandler(this._centerBox, 'center', this.panelId);
        this._rightBoxDNDHandler = new PanelZoneDNDHandler(this._rightBox, 'right', this.panelId);

        this.addContextMenuToPanel(this.panelPosition);

        Main.layoutManager.addChrome(this, { addToWindowgroup: false });
        this._moveResizePanel();
        this._onPanelEditModeChanged();
        this._processPanelAutoHide();

        this.connect('queue-relayout', () => this._setPanelHeight());

        this._signalManager.connect(global.settings, "changed::" + PANEL_AUTOHIDE_KEY, this._processPanelAutoHide, this);
        this._signalManager.connect(global.settings, "changed::" + PANEL_HEIGHT_KEY, this._moveResizePanel, this);
        this._signalManager.connect(global.settings, "changed::" + PANEL_ZONE_ICON_SIZES, this._onPanelZoneSizesChanged, this);
        this._signalManager.connect(global.settings, "changed::" + PANEL_ZONE_SYMBOLIC_ICON_SIZES, this._onPanelZoneSizesChanged, this);
        this._signalManager.connect(global.settings, "changed::" + PANEL_ZONE_TEXT_SIZES, this._onPanelZoneSizesChanged, this);
        this._signalManager.connect(global.settings, "changed::panel-edit-mode", this._onPanelEditModeChanged, this);
        this._signalManager.connect(global.settings, "changed::no-adjacent-panel-barriers", this._updatePanelBarriers, this);

        this._onPanelZoneSizesChanged();
    }

    /**
     * height:
     *
     * Deprecated: Third-party applets should use Applet._panelHeight (available since 4.0).
     * For the actual actor pixel height, use panel.get_height().
     */
    get height() {
        global.logWarning(`panel.height should no longer be accessed. ` +
            `Third-party applets should use applet._panelHeight (available since 4.0) ` +
            `to get the panel size for zone/icon calculations. Use panel.get_height() ` +
            `for the actual actor height.`);
        return this.heightForZones;
    }

    /**
     * updatePosition:
     * @monitorIndex: integer, index of monitor
     * @panelPosition, integer, where the panel should be placed
     *
     * Moves the panel to the monitor @monitorIndex and position @panelPosition
     */
    updatePosition(monitorIndex, panelPosition) {
        this.monitorIndex = monitorIndex
        this.panelPosition = panelPosition;
        this._positionChanged = true;

        this.monitor = global.display.get_monitor_geometry(monitorIndex);

        this._set_orientation();

        this.addContextMenuToPanel(panelPosition);
        this.addPanelStyleClass(panelPosition);
        this._moveResizePanel();
    }

    /**
     * addContextMenuToPanel:
     * @panelPosition, integer
     *
     *  Adds a context menu to the panel
     */
    addContextMenuToPanel(panelPosition) {
        switch (panelPosition)
        {
            case PanelLoc.top:
                this._context_menu = new PanelContextMenu(this, St.Side.TOP, this.panelId);
                break;
            case PanelLoc.bottom:
                this._context_menu = new PanelContextMenu(this, St.Side.BOTTOM, this.panelId);
                break;
            case PanelLoc.left:
                this._context_menu = new PanelContextMenu(this, St.Side.LEFT, this.panelId);
                break;
            case PanelLoc.right:
                this._context_menu = new PanelContextMenu(this, St.Side.RIGHT, this.panelId);
                break;
            default:
                global.log("addContextMenuToPanel - unrecognised panel position "+panelPosition);
        }
        this._menus.addMenu(this._context_menu);
    }

     /**
     * addPanelStyleClass:
     * @panelPosition, integer
     *
     *  Adds the panel style class.  NB the original #panel style class is kept
     */
    addPanelStyleClass(panelPosition) {
        switch (panelPosition)
        {
            case PanelLoc.top:
                this.remove_style_class_name('panel-bottom');
                this.remove_style_class_name('panel-left');
                this.remove_style_class_name('panel-right');
                this.add_style_class_name('panel-top');
                break;
            case PanelLoc.bottom:
                this.remove_style_class_name('panel-top');
                this.remove_style_class_name('panel-left');
                this.remove_style_class_name('panel-right');
                this.add_style_class_name('panel-bottom');
                break;
            case PanelLoc.left:
                this.remove_style_class_name('panel-bottom');
                this.remove_style_class_name('panel-top');
                this.remove_style_class_name('panel-right');
                this.add_style_class_name('panel-left');
                break;
            case PanelLoc.right:
                this.remove_style_class_name('panel-bottom');
                this.remove_style_class_name('panel-left');
                this.remove_style_class_name('panel-top');
                this.add_style_class_name('panel-right');
                break;
            default:
                global.log("addPanelStyleClass - unrecognised panel position "+panelPosition);
        }
    }

    /**
     * destroy:
     * @removeIconSizes (boolean): (optional) whether to remove zone icon size settings. Default value is true.
     *
     * Destroys the panel
     */
    destroy(removeIconSizes = true) {
        if (this._destroyed)
            return;

        this._destroyed = true;    // set this early so that any routines triggered during
                                   // the destroy process can test it

        Main.layoutManager.removeChrome(this);

        if (removeIconSizes)
            this._removeZoneIconSizes();
        // remove icon size settings if requested
        // settings should be removed except when panel is destroyed due to monitor change
        // this prevents settings from being reset every time a monitor is disconnected

        this._clearPanelBarriers();
        AppletManager.unloadAppletsOnPanel(this.panelId);
        this._context_menu.close();
        this._context_menu.destroy();

        this._leftBox.destroy();
        this._centerBox.destroy();
        this._rightBox.destroy();

        this._signalManager.disconnectAllSignals()

        this._menus = null;
        this.monitor = null;

        super.destroy();
    }

    /**
     * revealPanel:
     *
     * Immediately slides the panel in, cancelling any pending show/hide and
     * skipping the show delay. Has no effect while the panel is disabled.
     * The panel remains subject to the normal visibility logic afterwards.
     */
    revealPanel() {
        if (this._destroyed)
            return;

        if (this._showHideTimer > 0) {
            Mainloop.source_remove(this._showHideTimer);
            this._showHideTimer = 0;
        }

        this._showPanel();
    }

    peekPanel() {
        if (!this._hidden || this._peeking)
            return;

        this._peeking = true;
        this.revealPanel();

        Mainloop.timeout_add(PANEL_PEEK_TIME, () => {
            this._peeking = false;
            this._updatePanelVisibility();
            return false;
        });
    }

    /**
     * highlight:
     * @highlight (boolean): whether to turn on or off
     *
     * Turns on/off the highlight of the panel
     */
    highlight(highlight) {
        if (highlight == this.has_style_pseudo_class('highlight'))
            return;

        this.change_style_pseudo_class('highlight', highlight);

        this._highlighted = highlight;
        this._updatePanelVisibility();
    }

    /**
     * isHideable:
     *
     * Returns: whether the panel can be hidden (auto-hide or intellihide)
     */
    isHideable() {
        return this._autohideSettings != "false";
    }

    /**
     * _getProperty
     * @key (string): name of gsettings key
     * @type (string): (optional) type of data requested. "b" for boolean, "i" for integer. Default value is string
     *
     * Gets the desired property of the panel from gsettings
     *
     * Returns: property required
     */
    _getProperty(key, type){
        let values = global.settings.get_strv(key);
        let property;
        for (let i = 0; i < values.length; i++) {
            if (values[i].split(":")[0]==this.panelId) {
                property = values[i].split(":")[1];
                break;
            }
        }
        if (!property) {
            property = DEFAULT_PANEL_VALUES[key];
            values.push(this.panelId + ":" + property);
            global.settings.set_strv(key, values);
        }
        switch (type) {
            case "b":
                return property == "true";
            case "i":
                return parseInt(property);
            default:
                return property;
        }
    }

    /**
     * _getJSONProperty
     * @key (string): name of gsettings key
     *
     * Gets the desired JSON encoded property of the panel from gsettings
     *
     * Returns: property required
     */
    _getJSONProperty(key){
        let json = global.settings.get_string(key);

        Util.tryFn(function() {
            json = JSON.parse(json);
        }, function(err) {
            err.message = `Failed to parse JSON property: \n${err.message}`;
            global.logError(err);
            json = null;
        });

        return json;
    }

    handleDragOver(source, actor, x, y, time) {
//
// For empty panels. If over left,right,center box then will not get here.
//
        this._enterPanel();
        if (this._dragShowId && this._dragShowId > 0)
            Mainloop.source_remove(this._dragShowId);

        let leaveIfOut = () => {
            this._dragShowId = 0;
            let [x, y, whatever] = global.get_pointer();
            this.sync_hover();

            if (this.x < x && x < this.x + this.get_width() &&
                this.y < y && y < this.y + this.get_height()) {
                return true;
            } else {
                this._leavePanel();
                return false;
            }
        };

        this._dragShowId = Mainloop.timeout_add(500, leaveIfOut);

        return DND.DragMotionResult.NO_DROP;
    }
    /**
     * _updatePanelBarriers:
     *
     * https://cgit.freedesktop.org/cgit/?url=xorg/proto/fixesproto/plain/fixesproto.txt
     */
    _updatePanelBarriers() {

        this._clearPanelBarriers();

        if (this._destroyed)  // ensure we do not try to set barriers if panel is being destroyed
            return;

        let n_monitors = global.display.get_n_monitors();
        if (n_monitors == 1 || this.monitorIndex < 0 || this.monitorIndex >= n_monitors)  // skip panels that never got created
            return;

        let screen_width  = global.screen_width;
        let screen_height = global.screen_height;

        let noBarriers = global.settings.get_boolean("no-adjacent-panel-barriers");

        if (this.get_height() && this.get_width()) {
            let panelTop = 0;
            let panelBottom = 0;
            let panelLeft = 0;
            let panelRight = 0;

            if (!noBarriers) {   // barriers are required
                if (this.panelPosition == PanelLoc.top || this.panelPosition == PanelLoc.bottom) {
                    switch (this.panelPosition) {
                        case PanelLoc.top:
                            panelTop    = this.monitor.y;
                            panelBottom = this.monitor.y + this.get_height();
                            break;
                        case PanelLoc.bottom:
                            panelTop    = this.monitor.y + this.monitor.height - Math.floor(this.get_height());
                            panelBottom = this.monitor.y + this.monitor.height -1;
                            break;
                    }
                    let x_coord = this.monitor.x + this.monitor.width - 1 - this.margin_right;
                    if (panelTop != panelBottom && x_coord >= 0)
                    {
                        if (screen_width > this.monitor.x + this.monitor.width - this.margin_right) {    // if there is a monitor to the right or panel offset into monitor
                            this._rightPanelBarrier = new Meta.Barrier({
                                display: global.display,
                                x1: x_coord, y1: panelTop,
                                x2: x_coord, y2: panelBottom,
                                directions: Meta.BarrierDirection.NEGATIVE_X  // permit moving in positive y direction for a top barrier
                            });
                        }

                        x_coord = this.monitor.x + this.margin_left;
                        if (x_coord > 0) {                                    // if there is a monitor to the left or panel offset into monitor
                            this._leftPanelBarrier = new Meta.Barrier({
                                display: global.display,
                                x1: x_coord, y1: panelTop,
                                x2: x_coord, y2: panelBottom,
                                directions: Meta.BarrierDirection.POSITIVE_X  // permit moving in positive y direction for a top barrier
                            });
                        }
                    }
                } else {
                    switch (this.panelPosition) {
                        case PanelLoc.left:
                            panelLeft  = this.monitor.x;
                            panelRight = this.monitor.x + Math.floor(this.get_width());
                            break;
                        case PanelLoc.right:
                            panelLeft  = this.monitor.x + this.monitor.width - Math.floor(this.get_width());
                            panelRight = this.monitor.x + this.monitor.width-1;
                            break;
                        default:
                            global.log("updatePanelBarriers - unrecognised panel position "+this.panelPosition);
                    }
                    if (panelRight != panelLeft) {
                        if (this.toppanelHeight === 0) {
                            let y_coord = this.monitor.y + Math.floor(this.toppanelHeight) + this.margin_top;
                            if (y_coord > 0) {                                  // if there is a monitor above or top of panel offset into monitor
                                this._topPanelBarrier = new Meta.Barrier({
                                    display: global.display,
                                    x1: panelLeft, y1: y_coord,
                                    x2: panelRight, y2: y_coord,
                                    directions: Meta.BarrierDirection.POSITIVE_Y  // permit moving in positive y direction for a top barrier
                                });
                            }
                        }

                        if (this.bottompanelHeight === 0) {
                            let y_coord = this.monitor.y + this.monitor.height - Math.floor(this.bottompanelHeight)- this.margin_bottom -1;
                            if (screen_height > this.monitor.y + this.monitor.height         // if there is a monitor below
                                || this.bottompanelHeight > 0 || this.margin_bottom > 0) {
                                this._bottomPanelBarrier = new Meta.Barrier({
                                    display: global.display,
                                    x1: panelLeft, y1: y_coord,
                                    x2: panelRight, y2: y_coord,
                                    directions: Meta.BarrierDirection.NEGATIVE_Y
                                });
                            }
                        }
                    }
                }
            } else {        // barriers are not required
                this._clearPanelBarriers();
            }
        } else {            // actor without width or height
            this._clearPanelBarriers();
        }
    }

    _clearPanelBarriers() {
        if (this._leftPanelBarrier)
            this._leftPanelBarrier.destroy();
        if (this._rightPanelBarrier)
            this._rightPanelBarrier.destroy();
        if (this._topPanelBarrier)
            this._topPanelBarrier.destroy();
        if (this._bottomPanelBarrier)
            this._bottomPanelBarrier.destroy();

        this._leftPanelBarrier = null;
        this._rightPanelBarrier = null;
        this._topPanelBarrier = null;
        this._bottomPanelBarrier = null;
    }

    _onPanelEditModeChanged() {
        let old_mode = this._panelEditMode;

        this._panelEditMode = global.settings.get_boolean("panel-edit-mode");
        this._leftBox.change_style_pseudo_class('dnd', this._panelEditMode);
        this._centerBox.change_style_pseudo_class('dnd', this._panelEditMode);
        this._rightBox.change_style_pseudo_class('dnd', this._panelEditMode);

        /* this next section is a bit of a kludge and should be reworked when
           someone can find a better way. The issue is that boxlayout left and right
           align can show no visible box when containing no applets.  This puts a
           fixed min size in to permit a drop to happen in edit mode, it turns on
           when selecting edit mode, and off when leaving.

           Note that setting up to use the full width does not work, it gets
           left alignment which doesn't seem to be able to be over-ridden,
           and the applet gets a whole box fill effect which is weird when dragging
           - perhaps x_fill etc. is turned on elsewhere  */

        if (this._panelEditMode) {
            if (this.is_vertical) {
                let height = this._rightBox.get_height();
                if (height < EDIT_MODE_MIN_BOX_SIZE * global.ui_scale)
                    this._rightBox.set_height(EDIT_MODE_MIN_BOX_SIZE * global.ui_scale);
                height = this._leftBox.get_height();
                if (height < EDIT_MODE_MIN_BOX_SIZE * global.ui_scale)
                    this._leftBox.set_height(EDIT_MODE_MIN_BOX_SIZE * global.ui_scale);
            } else {
                let width = this._rightBox.get_width();
                if (width < EDIT_MODE_MIN_BOX_SIZE * global.ui_scale)
                    this._rightBox.set_width(EDIT_MODE_MIN_BOX_SIZE * global.ui_scale);
                width = this._leftBox.get_width();
                if (width < EDIT_MODE_MIN_BOX_SIZE * global.ui_scale)
                    this._leftBox.set_width(EDIT_MODE_MIN_BOX_SIZE * global.ui_scale);
            }
        } else {
            this._rightBox.set_size(-1, -1);
            this._leftBox.set_size(-1, -1);
        }

        if (old_mode != this._panelEditMode) {
            this._updatePanelVisibility();
        }

        this.queue_relayout();
    }

    vfunc_button_press_event(event) {
        if (event.button === 1) {
            if (this._context_menu.isOpen)
                this._context_menu.toggle();
        }
        if (event.button === 3) {
            try {
                let { x, y } = event;
                let target = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);

                // NB test on parent fails with centre aligned vertical box, but works for the test against the actor
                if (this._context_menu._getMenuItems().length > 0 &&
                   (target.get_parent() === this || target === this)) {
                    if (!this._context_menu.isOpen) {
                        switch (this.panelPosition) {
                            case PanelLoc.top:
                            case PanelLoc.bottom:
                                this._context_menu.shiftToPosition(x);
                                break;
                            case PanelLoc.left:
                            case PanelLoc.right:
                                this._context_menu.shiftToPosition(y);
                                break;
                        }
                    }

                    this._context_menu.toggle();
                }
            } catch(e) {
                global.log(e);
            }
        }

        return Clutter.EVENT_STOP;
    }

    _onFocusChanged() {
        if (global.display.focus_window && this._focusWindow !== undefined &&
            this._focusWindow == global.display.focus_window)
            return;

        this._signalManager.disconnect("position-changed");
        this._signalManager.disconnect("size-changed");

        if (!global.display.focus_window)
            return;

        this._focusWindow = global.display.focus_window;
        this._signalManager.connect(this._focusWindow, "position-changed", this._updatePanelVisibility, this);
        this._signalManager.connect(this._focusWindow, "size-changed", this._updatePanelVisibility, this);
        this._updatePanelVisibility();
    }

    _processPanelAutoHide() {
        this._autohideSettings = this._getProperty(PANEL_AUTOHIDE_KEY, "s");

        if (this._autohideSettings == "intel") {
            this._signalManager.connect(global.display, "notify::focus-window", this._onFocusChanged, this);
            /* focus-window signal is emitted when the workspace change
             * animation starts. When the animation ends, we do the position
             * check again because the windows have moved. We cannot use
             * _onFocusChanged because _onFocusChanged does nothing when there
             * is no actual focus change. */
            this._signalManager.connect(global.window_manager, "switch-workspace-complete", this._updatePanelVisibility, this);
            this._onFocusChanged();
        } else {
            this._signalManager.disconnect("notify::focus-window");
            this._signalManager.disconnect("switch-workspace-complete");
            this._signalManager.disconnect("position-changed");
            this._signalManager.disconnect("size-changed");
        }

        this._updatePanelVisibility();
        Main.layoutManager._chrome.modifyActorParams(this, { affectsStruts: this._autohideSettings == "false" });
    }
    /**
     * _getScaledPanelHeight:
     *
     * Function to calculate the desired panel height
     *
     * returns : panelheight
     */
    _getScaledPanelHeight() {
        let panelHeight = 0;
        panelHeight = this._getProperty(PANEL_HEIGHT_KEY, "i") * global.ui_scale;
        return panelHeight < 20 ? 40 : panelHeight;
    }

   /**
    * _setClipRegion:
    * @hidden: whether the panel should be clipped for hide
    * @offset: (optional): x or y position offset
    *
    * If @hidden is true the clip region is set to the one exposed strip of pixels
    * adjacent to the monitor edge. Otherwise, the clip region is set to the panel
    * size plus the shadow on the side of the panel opposite the monitor edge.
    *
    * @offset is only used during tweens. If provided, it is used to offset the
    * current position in order to calculate the exposed size.
    */
    _setClipRegion(hidden, offset) {
        if (!this._shouldClipPanel()) {
            // important during monitor layout changes.
            this.remove_clip(); // no-op if there wasn't any clipping to begin with.
            Main.layoutManager.updateChrome()
            return;
        }

        let animating = typeof offset === "number";
        let isHorizontal = this.panelPosition == PanelLoc.top
                           || this.panelPosition == PanelLoc.bottom;

        // determine exposed amount of panel
        let exposedAmount;
        if (isHorizontal) {
            if (hidden)
                exposedAmount = animating ? Math.abs(this.y - offset) + 1
                                          : 1;
            else
                exposedAmount = animating ? Math.abs(this.y - offset)
                                          : this.get_height();
        } else {
            if (hidden)
                exposedAmount = animating ? Math.abs(this.x - offset) + 1
                                          : 1;
            else
                exposedAmount = animating ? Math.abs(this.x - offset)
                                          : this.get_width();
        }

        // determine offset & set clip
        // top/left panels: must offset by the hidden amount
        // bottom/right panels: if showing must offset by shadow size
        // all panels: if showing increase exposedAmount by shadow size

        // we use only the shadowbox x1 or y1 (offset) to determine shadow size
        // as some themes use an offset shadow to draw only on one side whereas
        // others have a shadow all around. using the offset should handle
        // both cases.
        if (isHorizontal) {
            let clipOffsetY = 0;
            if (this.panelPosition == PanelLoc.top) {
                clipOffsetY = this.get_height() - exposedAmount;
            } else {
                if (!hidden)
                    clipOffsetY = this._shadowBox.y1;
            }
            if (!hidden)
                exposedAmount += Math.abs(this._shadowBox.y1);
            this.set_clip(0, clipOffsetY, this.get_width(), exposedAmount);
        } else {
            let clipOffsetX = 0;
            if (this.panelPosition == PanelLoc.left) {
                clipOffsetX = this.get_width() - exposedAmount;
            } else {
                if (!hidden)
                    clipOffsetX = this._shadowBox.x1;
            }
            if (!hidden)
                exposedAmount += Math.abs(this._shadowBox.x1);
            this.set_clip(clipOffsetX, 0, exposedAmount, this.get_height());
        }
        // Force the layout manager to update the input region
        Main.layoutManager.updateChrome()
    }

    _shouldClipPanel() {
        const n_monitors = global.display.get_n_monitors();

        if (n_monitors === 1)  {
            return false;
        }

        for (let i = 0; i < n_monitors; i++) {
            if (i === this.monitorIndex) {
                continue;
            }

            const m_rect = global.display.get_monitor_geometry(i);
            const test_rect = new Meta.Rectangle();

            switch (this.panelPosition) {
                case PanelLoc.top:
                case PanelLoc.bottom:
                    test_rect.x = this.x;
                    test_rect.width = this.get_width();
                    test_rect.height = this.heightForZones;

                    if (this.panelPosition === PanelLoc.top) {
                        test_rect.y = this.monitor.y - this.heightForZones;
                    } else {
                        test_rect.y = this.monitor.y + this.monitor.height;
                    }
                    break;
                case PanelLoc.left:
                case PanelLoc.right:
                    test_rect.y = this.y;
                    test_rect.height = this.get_height();
                    test_rect.width = this.heightForZones;

                    if (this.panelPosition === PanelLoc.left) {
                        test_rect.x = this.monitor.x - this.heightForZones;
                    } else {
                        test_rect.x = this.monitor.x + this.monitor.width;
                    }
                    break;
            }

            if (m_rect.overlap(test_rect)) {
                return true;
            }
        }

        return false;
    }

    vfunc_style_changed() {
        super.vfunc_style_changed();

        this._moveResizePanel();
    }

    /**
     * _moveResizePanel:
     *
     * Function to update the panel position, size, and clip region according to settings
     * values.  Note that this is also called when the style changes.
     */
    _moveResizePanel() {
        if (this._destroyed)
            return false;

        //
        // layouts set to be full width horizontal panels, and vertical panels set to use as much available space as is left
        //
        // NB If you want to use margin to inset the panels within a monitor, then you can't just set it here
        // else full screen windows will then go right to the edge with the panels floating over
        //
        this.monitor = global.display.get_monitor_geometry(this.monitorIndex);
        let horizontal_panel = (!!((this.panelPosition == PanelLoc.top || this.panelPosition == PanelLoc.bottom)));

        // this stands for width on vertical panels, and height on horizontal panels
        let panelHeight = this._getScaledPanelHeight();

        // find heights used by horizontal panels to determine height available for vertical panels.
        // we need to check Main.panelManager because this can be called before it has initialized.
        this.toppanelHeight = 0;
        this.bottompanelHeight = 0;
        if (Main.panelManager && !horizontal_panel)
            [this.toppanelHeight, this.bottompanelHeight] = heightsUsedMonitor(this.monitorIndex, Main.panelManager.panels);
        // get shadow and margins
        let themeNode = this.get_theme_node();

        // FIXME: inset shadows will probably break clipping.
        // I haven't seen a theme with inset panel shadows, but if there
        // are any then we need to just use the dummy shadow box in that case.
        let shadowBox;
        let shadow = themeNode.get_box_shadow();
        if (shadow) {
            shadowBox = new Clutter.ActorBox;
            let actorBox = new Clutter.ActorBox;
            shadow.get_box(actorBox, shadowBox);
        } else {
            // if we don't actually have a shadow, just create a dummy shadowBox
            shadowBox = {x1: 0, y1: 0, x2: 0, y2: 0};
        }

        let newMarginTop = 0;
        let newMarginBottom = 0;
        let newMarginLeft = 0;
        let newMarginRight = 0;
        try {
            newMarginTop    = themeNode.get_margin(St.Side.TOP);
            newMarginBottom = themeNode.get_margin(St.Side.BOTTOM);
            newMarginLeft   = themeNode.get_margin(St.Side.LEFT);
            newMarginRight  = themeNode.get_margin(St.Side.RIGHT);
        } catch (e) {
            global.log(e);
        }

        let panelChanged = false;

        let shadowChanged = !this._shadowBox
                            || shadowBox.x1 != this._shadowBox.x1
                            || shadowBox.x2 != this._shadowBox.x2
                            || shadowBox.y1 != this._shadowBox.y1
                            || shadowBox.y2 != this._shadowBox.y2;

        // if the shadow changed, we need to update the clip
        if (shadowChanged) {
            panelChanged = true;
            this._shadowBox = shadowBox;
        }

        // if the position changed, make sure the panel is showing
        // so it's more apparent that the panel moved successfully
        if (this._positionChanged) {
            panelChanged = true;
            this._positionChanged = false;
            this._showPanel();
        }

        // if the monitors changed, force update in case the position needs updating
        if (this._monitorsChanged) {
            panelChanged = true;
            this._monitorsChanged = false;
        }

        // calculate new panel sizes.  NB margin is already scaled for hidpi
        let newVertPanelHeight = this.monitor.height - this.toppanelHeight - this.bottompanelHeight
                                 - (newMarginTop + newMarginBottom);
        let newHorizPanelWidth = this.monitor.width - (newMarginLeft + newMarginRight);

        // and determine if this panel's size changed
        if (horizontal_panel) {
            if (this.get_width() != newHorizPanelWidth || this.get_height() != panelHeight)
                panelChanged = true;
        } else {
            if (this.get_width() != panelHeight || this.get_height() != newVertPanelHeight)
                panelChanged = true;
        }

        if (panelChanged) {
            // remove any tweens that might be active for autohide
            this.remove_all_transitions();

            this.margin_top = newMarginTop;
            this.margin_bottom = newMarginBottom;
            this.margin_left = newMarginLeft;
            this.margin_right = newMarginRight;

            // update size and determine position depending on hidden state
            let newX, newY;
            if (horizontal_panel) {
                newX = this.monitor.x;
                if (this.panelPosition == PanelLoc.top) {
                    newY = this._hidden ? this.monitor.y - panelHeight + 1
                                        : this.monitor.y;
                } else {
                    newY = this._hidden ? this.monitor.y + this.monitor.height - 1
                                        : this.monitor.y + this.monitor.height - panelHeight;
                }
                this.set_size(newHorizPanelWidth, panelHeight);
            } else {
                newY = this.monitor.y + this.toppanelHeight;
                if (this.panelPosition == PanelLoc.left) {
                    newX = this._hidden ? this.monitor.x - panelHeight + 1
                                        : this.monitor.x;
                } else {
                    newX = this._hidden ? this.monitor.x + this.monitor.width - 1
                                        : this.monitor.x + this.monitor.width - panelHeight;
                }
                this.set_size(panelHeight, newVertPanelHeight);
            }

            // update position and clip region
            this.set_position(newX, newY)
            this._setClipRegion(this._hidden);

            // only needed here for when this routine is called when the style changes
            this._updatePanelBarriers();

            this._updatePanelVisibility();

            // If we are adjusting the heights of horizontal panels then the vertical ones on this monitor
            // need to be changed at the same time.
            if (Main.panelManager && horizontal_panel) {
                let panels = Main.panelManager.getPanelsInMonitor(this.monitorIndex);
                for (let p = 0, len = panels.length; p < len; p++) {
                    if (panels[p].panelPosition == PanelLoc.left || panels[p].panelPosition == PanelLoc.right)
                        panels[p]._moveResizePanel();
                }
            }

            // AppletManager might not be initialized yet
            if (AppletManager.appletsLoaded) this._setPanelHeight();
        }
        return true;
    }

    _set_orientation() {
        //
        // cater for the style/alignment for different panel orientations
        //
        if (this.panelPosition == PanelLoc.top || this.panelPosition == PanelLoc.bottom) {
            this._set_horizontal_panel_style();
            this.is_vertical = false;
        } else {
            this._set_vertical_panel_style();
            this.is_vertical = true;
        }
    }

    _set_vertical_panel_style() {
        this._leftBox.add_style_class_name('vertical');
        this._leftBox.set_vertical(true);
        this._leftBox.set_x_align(Clutter.ActorAlign.FILL);
        this._leftBox.set_y_align(Clutter.ActorAlign.START);

        this._centerBox.add_style_class_name('vertical');
        this._centerBox.set_vertical(true);
        this._centerBox.set_x_align(Clutter.ActorAlign.FILL);
        this._centerBox.set_y_align(Clutter.ActorAlign.FILL);

        this._rightBox.add_style_class_name('vertical');
        this._rightBox.set_vertical(true);
        this._rightBox.set_x_align(Clutter.ActorAlign.FILL);
        this._rightBox.set_y_align(Clutter.ActorAlign.END);
    }

    _set_horizontal_panel_style() {
        this._leftBox.remove_style_class_name('vertical');
        this._leftBox.set_vertical(false);
        this._leftBox.set_x_align(Clutter.ActorAlign.START);
        this._leftBox.set_y_align(Clutter.ActorAlign.FILL);

        this._centerBox.remove_style_class_name('vertical');
        this._centerBox.set_vertical(false);
        this._centerBox.set_x_align(Clutter.ActorAlign.FILL);
        this._centerBox.set_y_align(Clutter.ActorAlign.FILL);

        this._rightBox.remove_style_class_name('vertical');
        this._rightBox.set_vertical(false);
        this._rightBox.set_x_align(Clutter.ActorAlign.END);
        this._rightBox.set_y_align(Clutter.ActorAlign.FILL);
    }

    _setPanelHeight() {
        let height = setHeightForPanel(this);
        if (height === this.heightForZones)
            return;

        this.heightForZones = height;

        // In case icon sizes are responding to panel height
        this._onPanelZoneSizesChanged();

        this.emit('size-changed', height);
    }

    _createEmptyZoneSizes() {
        let typeStruct = {
            "left" : 0,
            "center" : 0,
            "right" : 0
        };

        let sizes = {
            "fullcolor" : typeStruct,
            "symbolic" : typeStruct,
            "text" : typeStruct
        };

        return sizes;
    }

    _onPanelZoneSizesChanged(value, key) {
        if (this._destroyed)
            return;

        let changed = false;
        let oldZoneSizes = this._panelZoneSizes;

        let sizeSets = [
            ["fullcolor", PANEL_ZONE_ICON_SIZES, (a,b,c,d) => this._clampPanelZoneColorIconSize(a,b,c,d), DEFAULT_FULLCOLOR_ICON_SIZE_VALUES],
            ["symbolic", PANEL_ZONE_SYMBOLIC_ICON_SIZES, (a,b,c,d) => this._clampPanelZoneSymbolicIconSize(a,b,c,d), DEFAULT_SYMBOLIC_ICON_SIZE_VALUES],
            ["text", PANEL_ZONE_TEXT_SIZES, (a,b,c,d) => this._clampPanelZoneTextSize(a,b,c,d), DEFAULT_TEXT_SIZE_VALUES]
        ];

        /* Iterate thru the sizeSets to get our sizes for all 3 types */
        sizeSets.forEach( sizeSet => {
            let [typeString, settingKey, getSizeFunc, defaults] = sizeSet;

            let settingsArray = this._getJSONProperty(settingKey);

            /* Temporarily disconnect setting handler, so we don't trigger for our own update */
            this._signalManager.disconnect("changed::" + settingKey);

            /* If one of the sizing key contains nothing, reset them to default before continuing */
            if (!settingsArray) {
                log(`Panel zone size settings invalid, resetting org.cinnamon "${settingKey}"`);
                global.settings.reset(settingKey);
                settingsArray = this._getJSONProperty(settingKey);
            }

            let haveSettings = false;

            /* Now, iterate thru the individual set's values (an individual setting key's array of panel sizes),
             * then compute their display sizes and stick them in this._panelZoneSizes for the current type. */
            settingsArray.forEach( sizes => {
                if (sizes.panelId !== this.panelId)
                    return;

                haveSettings = true;

                sizes.left = getSizeFunc(sizes, typeString, "left", defaults);
                sizes.center = getSizeFunc(sizes, typeString, "center", defaults);
                sizes.right = getSizeFunc(sizes, typeString, "right", defaults);

                let zoneCache = oldZoneSizes[typeString];

                if (sizes.left !== zoneCache.left ||
                    sizes.center !== zoneCache.center ||
                    sizes.right !== zoneCache.right) {
                    changed = true;
                }

                this._panelZoneSizes[typeString] = sizes;
            });

            /* If there are no settings for this panel (it's either new, or the settings key has been reset to defaults),
             * generate default display values for adding to this._panelZoneSizes, as well as a default item to add to
             * gsettings. */
            if (!haveSettings) {
                let panelHeight = this.heightForZones * global.ui_scale;

                let defaultForCache = {
                    "left": getSizeFunc(defaults, typeString, "left", null),
                    "center": getSizeFunc(defaults, typeString, "center", null),
                    "right": getSizeFunc(defaults, typeString, "right", null)
                };

                this._panelZoneSizes[typeString] = defaultForCache;

                let defaultSet = defaults;

                defaultSet["panelId"] = this.panelId;
                settingsArray.push(defaultSet);
                global.settings.set_string(settingKey, JSON.stringify(settingsArray));
           }

           this._signalManager.connect(global.settings, "changed::" + settingKey, this._onPanelZoneSizesChanged, this);
        });

        let zones = [
            [this._leftBox, "left"],
            [this._centerBox, "center"],
            [this._rightBox, "right"]
        ];

        zones.forEach( zone => {
            let [actor, zoneString] = zone;

            const value = this._panelZoneSizes["text"][zoneString];

            if (value > 0.0) {
                actor.set_style("font-size: %.1fpt;".format(value));
            } else {
                actor.set_style(null);
            }
        });

        if (changed)
            this.emit('icon-size-changed');
    }

    _clampPanelZoneTextSize(panelZoneSizeSet, typeString, zoneString, defaults) {
        let iconSize = panelZoneSizeSet.maybeGet(zoneString);

        if (iconSize == undefined) {
            iconSize = defaults[zoneString];
        }

        if (iconSize !== 0.0) {
            return iconSize.clamp(MIN_TEXT_SIZE_PTS, MAX_TEXT_SIZE_PTS);
        }

        return iconSize;
    }

    _clampPanelZoneColorIconSize(panelZoneSizeSet, typeString, zoneString, defaults) {
        let iconSize = panelZoneSizeSet.maybeGet(zoneString);

        if (iconSize == undefined) {
            iconSize = defaults[zoneString];
        }

        let height = this.heightForZones / global.ui_scale;

        if (iconSize === -1) { // Legacy: Scale to panel size
            iconSize = height;
        } else if (iconSize === 0 || iconSize > height) { // To best fit within the panel size
            iconSize = toStandardIconSize(height);
        }

        return iconSize; // Always return a value above 0 or St will spam the log.
    }

    _clampPanelZoneSymbolicIconSize(panelZoneSizeSet, typeString, zoneString, defaults) {
        let iconSize = panelZoneSizeSet.maybeGet(zoneString);

        if (iconSize == undefined) {
            iconSize = defaults[zoneString];
        }

        let panelHeight = this.heightForZones / global.ui_scale;

        let new_size = iconSize.clamp(MIN_SYMBOLIC_SIZE_PX, Math.min(MAX_SYMBOLIC_SIZE_PX, panelHeight));
        return new_size;
    }

    getPanelZoneIconSize(locationLabel, iconType) {
        let zoneConfig = this._panelZoneSizes;
        let typeString = "fullcolor";

        if (iconType == St.IconType.SYMBOLIC) {
            typeString = "symbolic";
        }

        return this._panelZoneSizes[typeString][locationLabel];
    }

    _removeZoneIconSizes() {
        let sizeSets = [
            ["fullcolor", PANEL_ZONE_ICON_SIZES],
            ["symbolic", PANEL_ZONE_SYMBOLIC_ICON_SIZES],
            ["text", PANEL_ZONE_TEXT_SIZES]
        ];

        sizeSets.forEach( sizeSet => {
            let [typeString, settingKey] = sizeSet;

            let settingsArray = this._getJSONProperty(settingKey);

            /* Temporarily disconnect setting handler, so we don't trigger for our own update */
            this._signalManager.disconnect("changed::" + settingKey);

            const zoneIndex = settingsArray.findIndex( obj => {
                return obj && obj.panelId === this.panelId;
            });

            if (zoneIndex >= 0) {
                settingsArray.splice(zoneIndex, 1);

                this._panelZoneSizes = null;

                global.settings.set_string(settingKey, JSON.stringify(settingsArray));

                this._signalManager.connect(global.settings, "changed::" + settingKey, this._onPanelZoneSizesChanged, this);
            }
        });

        global.log(`[Panel ${this.panelId}] Removing zone configuration`);
    }

    /**
     * _calculateBoxes:
     * @allocWidth (real): allocated total width
     * @allocHeight (real): allocated total height
     * @isVertical (boolean): if on vertical panel
     *
     * Given the minimum and natural width requested by each box, this function
     * calculates how much width should actually allocated to each box. The
     * function returns two variables [@leftBoundary, @rightBoundary]
     *
     * The expected outcome of the code is as follows:
     *
     * Horizontal panels:
     * Assuming that the centerBox is filled, the primary objective is to center
     * the centerBox whenever possible. This will be done all the time unless doing
     * so requires some box's width to go under its minimum width.
     *
     * If we are centering the centerBox, there are two possible scenarios.
     * Firstly, if the centerBox can be perfectly centered while everything takes
     * their natural size, then everything will be allocated at least their natural
     * size such that the centerBox is centered, leftBox is left aligned, rightBox
     * is right aligned.
     *
     * Otherwise, we first allocate the minWidth to every box, and then distribute
     * the remaining space proportional to how much more space each box wants.
     * This is done in a way that ensures the leftWidth and rightWidth are equal.
     *
     * If it is not possible to center the centerBox, but there is enough space to
     * just allocate the boxes, the centerBox will be made as centered as possible
     * without making things go under their minWidth. This is achieved by making
     * the shorter box go to their min width, and distributing the remaining space
     * among the two other boxes.
     *
     * Finally, if there isn't even enough space to just put the things, the width
     * allocated is just proportional to the minimum width.
     *
     * In the cases where the centerBox is not occupied, a similar mechanism is
     * employed. If there is enough space for everything to get their natural
     * width, this will happen. Otherwise, we first allocate the minimum width and
     * then distribute the remaining space proportional to how much more space each
     * box wants. In the scenario where the isn't enough space to just allocate the
     * minimum width, we just allocate proportional to the minimum width.
     *
     * FIXME: consider replacing all of this with clutter constraints.  Fundamentally
     * we have three boxes constrained to be butted up against each other and to stretch
     * over the whole panel.  If the centre box is populated then it needs to be centred.
     * Any field has to be given a minimum size in edit mode to allow drag and drop.
     *
     * Returns (array): The left and right widths to be allocated.
     */
    _calculateBoxes(allocWidth, allocHeight, isVertical) {
        let leftBoundary = 0;
        let rightBoundary = 0;
        let leftMinWidth = 0;
        let leftNaturalWidth = 0;
        let rightMinWidth = 0;
        let rightNaturalWidth = 0;
        let centerMinWidth = 0;
        let centerNaturalWidth = 0;

        let centerBoxOccupied = this._centerBox.get_n_children() > 0;

        if (isVertical) {
            [leftMinWidth, leftNaturalWidth] = this._leftBox.get_preferred_height(-1);
            [centerMinWidth, centerNaturalWidth] = this._centerBox.get_preferred_height(-1);
            [rightMinWidth, rightNaturalWidth] = this._rightBox.get_preferred_height(-1);
        } else {
            [leftMinWidth, leftNaturalWidth] = this._leftBox.get_preferred_width(-1);
            [centerMinWidth, centerNaturalWidth] = this._centerBox.get_preferred_width(-1);
            [rightMinWidth, rightNaturalWidth] = this._rightBox.get_preferred_width(-1);
        }

        // If in panel edit mode, give the center box a minimum width to allow a visual
        // indicator of where to drag and drop
        if (this._panelEditMode) {
            centerBoxOccupied = true;
            centerMinWidth = Math.max(centerMinWidth, EDIT_MODE_MIN_BOX_SIZE * global.ui_scale);
            centerNaturalWidth = Math.max(centerNaturalWidth, EDIT_MODE_MIN_BOX_SIZE * global.ui_scale);
        }

        let totalMinWidth = leftMinWidth + centerMinWidth + rightMinWidth;
        let totalNaturalWidth = leftNaturalWidth + centerNaturalWidth + rightNaturalWidth;

        let sideMinWidth = Math.max(leftMinWidth, rightMinWidth);
        let sideNaturalWidth = Math.max(leftNaturalWidth, rightNaturalWidth);
        let totalCenteredMinWidth = centerMinWidth + 2 * sideMinWidth;
        let totalCenteredNaturalWidth = centerNaturalWidth + 2 * sideNaturalWidth;

        let leftWidth, rightWidth;

        if (centerBoxOccupied) {
            if (totalCenteredNaturalWidth < allocWidth) {
                /* center the central box and butt the left and right up to it. */
                leftWidth = (allocWidth - centerNaturalWidth) / 2;
                rightWidth = leftWidth;
            } else if (totalCenteredMinWidth < allocWidth) {
                /* Center can be centered as without shrinking things too much.
                 * First give everything the minWidth they want, and then
                 * distribute the remaining space proportional to how much the
                 * regions want. */
                let totalRemaining = allocWidth - totalCenteredMinWidth;
                let totalWant = totalCenteredNaturalWidth - totalCenteredMinWidth;

                leftWidth = sideMinWidth + (sideNaturalWidth - sideMinWidth) / totalWant * totalRemaining;
                rightWidth = leftWidth;
            } else if (totalMinWidth < allocWidth) {
                /* There is enough space for minWidth if we don't care about
                 * centering. Make center things as center as possible */
                if (leftMinWidth > rightMinWidth) {
                    leftWidth = leftMinWidth;

                    if (leftMinWidth + centerNaturalWidth + rightNaturalWidth < allocWidth) {
                        rightWidth = allocWidth - leftMinWidth - centerNaturalWidth;
                    } else {
                        let totalRemaining = allocWidth - totalMinWidth;
                        let totalWant = centerNaturalWidth + rightNaturalWidth - (centerMinWidth + rightMinWidth);

                        rightWidth = rightMinWidth;
                        if (totalWant > 0)
                            rightWidth += (rightNaturalWidth - rightMinWidth) / totalWant * totalRemaining;
                    }
                } else {
                    rightWidth = rightMinWidth;

                    if (rightMinWidth + centerNaturalWidth + leftNaturalWidth < allocWidth) {
                        leftWidth = allocWidth - rightMinWidth - centerNaturalWidth;
                    } else {
                        let totalRemaining = allocWidth - totalMinWidth;
                        let totalWant = centerNaturalWidth + leftNaturalWidth - (centerMinWidth + leftMinWidth);

                        leftWidth = leftMinWidth;
                        if (totalWant > 0)
                            leftWidth += (leftNaturalWidth - leftMinWidth) / totalWant * totalRemaining;
                    }
                }
            } else {
                /* Scale everything down according to their minWidth. */
                leftWidth = leftMinWidth / totalMinWidth * allocWidth;
                rightWidth = rightMinWidth / totalMinWidth * allocWidth;
            }
        } else {  // center box not occupied
            if (totalNaturalWidth < allocWidth) {
                /* Everything's fine. Allocate as usual. */
                if (isVertical) {
                    leftWidth = Math.max(leftNaturalWidth, leftMinWidth);
                    rightWidth = Math.max(rightNaturalWidth, rightMinWidth);
                } else {
                    leftWidth = leftNaturalWidth;
                    rightWidth = rightNaturalWidth;
                }
            } else if (totalMinWidth < allocWidth) {
                /* There is enough space for minWidth but not for naturalWidth.
                 * Allocate the minWidth and then divide the remaining space
                 * according to how much more they want. */
                let totalRemaining = allocWidth - totalMinWidth;
                let totalWant = totalNaturalWidth - totalMinWidth;

                leftWidth = leftMinWidth + ((leftNaturalWidth - leftMinWidth) / totalWant) * totalRemaining;
                rightWidth = rightMinWidth + ((rightNaturalWidth - rightMinWidth) / totalWant) * totalRemaining;
            } else {
                /* Scale everything down according to their minWidth. */
                leftWidth = leftMinWidth / totalMinWidth * allocWidth;
                rightWidth = rightMinWidth / totalMinWidth * allocWidth;
            }
        }

        leftBoundary = Math.round(leftWidth);
        rightBoundary = Math.round(allocWidth - rightWidth);

        if (!isVertical && (this.get_direction() === St.TextDirection.RTL)) {
            leftBoundary = Math.round(allocWidth - leftWidth);
            rightBoundary = Math.round(rightWidth);
        }

        return [leftBoundary, rightBoundary];
    }

    vfunc_allocate(box, flags) {
        this.set_allocation(box, flags);

        const allocWidth = box.x2 - box.x1;
        const allocHeight = box.y2 - box.y1;

        const childBox = new Clutter.ActorBox();

        if (this.panelPosition == PanelLoc.left || this.panelPosition == PanelLoc.right) {
            let [leftBoundary, rightBoundary] = this._calculateBoxes(allocHeight, allocWidth, true);

            childBox.x1 = 0;
            childBox.x2 = allocWidth;

            childBox.y1 = 0;
            childBox.y2 = leftBoundary;
            this._leftBox.allocate(childBox, flags);

            childBox.y1 = leftBoundary;
            childBox.y2 = rightBoundary;
            this._centerBox.allocate(childBox, flags);

            childBox.y1 = rightBoundary;
            childBox.y2 = allocHeight;
            this._rightBox.allocate(childBox, flags);
        } else {
            let [leftBoundary, rightBoundary] = this._calculateBoxes(allocWidth, allocHeight, false);
            const isRTL = this.get_direction() === St.TextDirection.RTL;

            childBox.y1 = 0;
            childBox.y2 = allocHeight;

            if (isRTL) {
                childBox.x1 = leftBoundary;
                childBox.x2 = allocWidth;
            } else {
                childBox.x1 = 0;
                childBox.x2 = leftBoundary;
            }
            this._leftBox.allocate(childBox, flags);

            if (isRTL) {
                childBox.x1 = rightBoundary;
                childBox.x2 = leftBoundary;
            } else {
                childBox.x1 = leftBoundary;
                childBox.x2 = rightBoundary;
            }
            this._centerBox.allocate(childBox, flags);

            if (isRTL) {
                childBox.x1 = 0;
                childBox.x2 = rightBoundary;
            } else {
                childBox.x1 = rightBoundary;
                childBox.x2 = allocWidth;
            }
            this._rightBox.allocate(childBox, flags);
        }
    }

    /**
     * _panelHasOpenMenus:
     *
     * Checks if panel has open menus in the global.menuStack
     * @returns
     */
    _panelHasOpenMenus() {
        if (global.menuStack == null || global.menuStack.length == 0)
            return false;

        for (let i = 0; i < global.menuStack.length; i++) {
            let menu = global.menuStack[i];
            if (menu.customStyleClass && menu.customStyleClass.includes("thumbnail"))
                continue;
            if (menu.getPanel() === this) {
                return true;
            }
        }

        return false;
    }

    /**
     * _updatePanelVisibility:
     *
     * Checks whether the panel should show based on the autohide settings and
     * position of mouse/active window. It then calls the _queueShowHidePanel
     * function to show or hide the panel as necessary.
     *
     * true = autohide, false = always show, intel = Intelligent
     */
    _updatePanelVisibility() {
        this._mouseEntered = this._mouseOnPanel();

        if (this._panelEditMode || this._highlighted || this._peeking ||
            this._panelHasOpenMenus() || Main.chromeRaiseManager.isPanelRaised(this))
            this._shouldShow = true;
        else {
            switch (this._autohideSettings) {
                case "false":
                    this._shouldShow = true;
                    break;
                case "true":
                    this._shouldShow = this._mouseEntered;
                    break;
                default:
                    if (this._mouseEntered || !global.display.focus_window ||
                        global.display.focus_window.get_window_type() == Meta.WindowType.DESKTOP) {
                        this._shouldShow = true;
                        break;
                    }

                    if (global.display.focus_window.get_monitor() != this.monitorIndex) {
                        this._shouldShow = false;
                        break;
                    }
                    let x, y;

                    /* Calculate the x or y instead of getting it from the actor since the
                    * actor might be hidden*/
                    switch (this.panelPosition) {
                        case PanelLoc.top:
                            y = this.monitor.y;
                            break;
                        case PanelLoc.bottom:
                            y = this.monitor.y + this.monitor.height - this.get_height();
                            break;
                        case PanelLoc.left:
                            x = this.monitor.x;
                            break;
                        case PanelLoc.right:
                            x = this.monitor.x + this.monitor.width - this.get_width();
                            break;
                        default:
                            global.log("updatePanelVisibility - unrecognised panel position "+this.panelPosition);
                    }

                    let rect = global.display.focus_window.get_frame_rect();
                    /* Magic to check whether the panel position overlaps with the
                    * current focused window */
                    if (this.panelPosition == PanelLoc.top || this.panelPosition == PanelLoc.bottom) {
                        this._shouldShow = !(Math.max(this.x, rect.x) < Math.min(this.x + this.get_width(), rect.x + rect.width) &&
                                            Math.max(y, rect.y) < Math.min(y + this.get_height(), rect.y + rect.height));
                    } else {
                        this._shouldShow = !(Math.max(x, rect.x) < Math.min(x + this.get_width(), rect.x + rect.width) &&
                                            Math.max(this.y, rect.y) < Math.min(this.y + this.get_height(), rect.y + rect.height));
                    }

            } // end of switch on autohidesettings
        }

        this._queueShowHidePanel();
    }

    /**
     * _queueShowHidePanel:
     *
     * Makes the panel show or hide after a delay specified by
     * panels-show-delay and panels-hide-delay.
     */
    _queueShowHidePanel() {
        if (this._showHideTimer) {
            Mainloop.source_remove(this._showHideTimer);
            this._showHideTimer = 0;
        }

        /* Use a timeout_add even if delay is 0 to avoid "flashing" of panel.
         * Otherwise, if, say hideDelay is 0 and showDelay is 1000, when you
         * move over an applet, leave and enter events are fired consecutively.
         * Then the leave-event causes the panel hides instantly, causing a
         * further leave-event (since the mouse actually left the panel), which
         * clears the showPanel timer, and the panel won't show up again. If a
         * timeout_add is used for showDelay, the hide timeout will be cancelled
         * by the coming enter-event, and the panel remains open. */
        if (this._shouldShow) {
            let showDelay = this._getProperty(PANEL_SHOW_DELAY_KEY, "i");
            this._showHideTimer = Mainloop.timeout_add(showDelay, this._showPanel.bind(this));
        } else {
            let hideDelay = this._getProperty(PANEL_HIDE_DELAY_KEY, "i");
            this._showHideTimer = Mainloop.timeout_add(hideDelay, this._hidePanel.bind(this));
        }
    }

    vfunc_enter_event() {
        this._enterPanel();
    }

    _enterPanel(actor=null, event=null) {
        if (!this._mouseEntered) {
            this._updatePanelVisibility();
        }
    }

    vfunc_leave_event(event) {
        this._leavePanel(null, event);
    }

    _leavePanel(actor=null, event=null) {
        // Panel gives false leave-event's when mouse is still on panel so we determine this._mouseEntered
        // manually with this._mouseOnPanel() in this._updatePanelVisibility()

        if (this._mouseEntered) {
            this._updatePanelVisibility();
            if (this.isHideable() && event !== null && this._mouseOnPanel()) {
                // Since we get false leave-event's and reported mouse position is often still
                // on panel even if left, we check again a short while later to make sure.
                setTimeout(this._updatePanelVisibility.bind(this), 250);
            }
        }
    }

    _mouseOnPanel() {
        this.sync_hover();
        const [x, y] = global.get_pointer();

        return (this.x <= x && x <= this.x + this.get_width() &&
            this.y <= y && y <= this.y + this.get_height());
    }

    /**
     * disable:
     *
     * Disables the panel by settings the opacity to 0 and hides if autohide is
     * enable. The actor is then hidden after the animation.
     */
    disable() {
        this._disabled = true;
        this.ease({
            opacity: 0,
            duration: AUTOHIDE_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this.hide();
            }
        });
    }

    /**
     * enable:
     *
     * Reverses the effects of the disable function.
     */
    enable() {
        this._disabled = false;
        this.show();
        this.ease({
            opacity: 255,
            duration: AUTOHIDE_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD
        });
    }

    /**
     * _showPanel:
     *
     * A function to force the panel to show. This has no effect if the panel
     * is disabled.
     */
    _showPanel() {
        this._showHideTimer = 0;

        if (this._disabled)
            return;

        if (!this._hidden)
            return;

        // setup panel tween - slide in from edge of monitor
        // if horizontal panel, animation on y. if vertical, animation on x.
        let isHorizontal = this.panelPosition == PanelLoc.top
                           || this.panelPosition == PanelLoc.bottom;
        let animationTime = AUTOHIDE_ANIMATION_TIME;
        let panelParams = { duration: animationTime,
                            mode: Clutter.AnimationMode.EASE_OUT_QUAD };

        // set up original and destination positions and add tween
        // destination parameter
        let origPos, destPos;
        if (isHorizontal) {
            let height = this.get_height();
            if (this.panelPosition == PanelLoc.top) {
                destPos = this.monitor.y;
                origPos = this.monitor.y - height;
            } else {
                destPos = this.monitor.y + this.monitor.height - height;
                origPos = this.monitor.y + this.monitor.height;
            }
            panelParams['y'] = destPos;
        } else {
            let width = this.get_width();
            if (this.panelPosition == PanelLoc.left) {
                destPos = this.monitor.x;
                origPos = this.monitor.x - width;
            } else {
                destPos = this.monitor.width - width + this.monitor.x;
                origPos = this.monitor.width + this.monitor.x;
            }
            panelParams['x'] = destPos;
        }

        // setup onUpdate tween parameter to set the actor clip region during animation.
        panelParams['onUpdate'] = () => this._setClipRegion(false, origPos);
        // setup boxes tween - fade in as panel slides
        let boxParams = { opacity: 255,
                          duration: animationTime * 2,
                          mode: Clutter.AnimationMode.EASE_OUT_QUAD };

        // show boxes and add tweens
        this._leftBox.show();
        this._centerBox.show();
        this._rightBox.show();
        this.ease(panelParams);
        this._leftBox.ease(boxParams);
        this._centerBox.ease(boxParams);
        this._rightBox.ease(boxParams);

        this._hidden = false;
    }

    /**
     * _hidePanel:
     *
     * This hides the panel.
     */
    _hidePanel() {
        if (this._destroyed)
            return;
        this._showHideTimer = 0;

        if (this._hidden)
            return;

        // setup panel tween - slide out the monitor edge leaving one pixel
        // if horizontal panel, animation on y. if vertical, animation on x.
        let isHorizontal = this.panelPosition == PanelLoc.top
                         || this.panelPosition == PanelLoc.bottom;
        let panelParams = { duration: AUTOHIDE_ANIMATION_TIME,
                            mode: Clutter.AnimationMode.EASE_OUT_QUAD };

        // setup destination position and add tween destination parameter
        // remember to always leave a vestigial 1px strip or the panel
        // will become inaccessible
        let destPos;
        if (isHorizontal) {
            let height = this.get_height();
            if (this.panelPosition == PanelLoc.top)
                destPos = this.monitor.y - height + 1;
            else
                destPos = this.monitor.y + this.monitor.height - 1;
            panelParams['y'] = destPos;
        } else {
            let width = this.get_width();
            if (this.panelPosition == PanelLoc.left)
                destPos = this.monitor.x - width + 1;
            else
                destPos = this.monitor.x + this.monitor.width - 1;
            panelParams['x'] = destPos;
        }

        // setup onUpdate tween parameter to update the actor clip region during animation
        panelParams['onUpdate'] = () => this._setClipRegion(true, destPos);
        // hide boxes after panel slides out
        panelParams['onComplete'] = () => {
               this._leftBox.hide();
               this._centerBox.hide();
               this._rightBox.hide();
        };

        // setup boxes tween - fade out as panel slides out
        let boxParams = { opacity: 0,
                          duration: AUTOHIDE_BOX_FADE_TIME,
                          mode: Clutter.AnimationMode.EASE_OUT_QUAD };

        // add all tweens
        this.ease(panelParams);
        this._leftBox.ease(boxParams);
        this._centerBox.ease(boxParams);
        this._rightBox.ease(boxParams);

        this._hidden = true;
    }

    getIsVisible() {
        return !this._hidden || this._shouldShow;
    }

    resetDNDZones() {
        this._leftBoxDNDHandler.reset();
        this._centerBoxDNDHandler.reset();
        this._rightBoxDNDHandler.reset();
    }
});
