/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of "Scrivo" nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * $Id: SplitLayout.js 616 2013-04-22 23:48:38Z geert $ */ "use strict"; SUI.SplitLayout = SUI.defineClass( /** @lends SUI.SplitLayout.prototype */{ /** @ignore */ baseClass: SUI.AnchorLayout, /** * @class * The split layOut is a container type that lets you split an area into a * center box and (optional) north, south, west and east boxes. When * resizing the center box will shrink or expand until the minimum size of * the content box of the center is reached. Shrinking further will cause * the north, south, west and east boxes to shrink until the minimum width * or height of their content boxes. * * @augments SUI.AnchorLayout * * @description * Construct a split layOut object. You can tell the split layOut which * areas (north/south/west/east) to set and the dimensions to use. * It's also possible to set the child boxes directly. * * @constructs * @param see base class * @param {Object} arg.center An object with the following members: * @param {SUI.Box} arg.center.box A client box (optional) * @param {Object} arg.north (optional) An object with the following * members: * @param {int} arg.north.height height of the north area * @param {SUI.Box} arg.north.box A client box (optional) * @param {Object} arg.south (optional) An object with the following * members: * @param {int} arg.south.height height of the south area * @param {SUI.Box} arg.south.box A client box (optional) * @param {Object} arg.west (optional) An object with the following * members: * @param {int} arg.west.width width of the west area * @param {SUI.Box} arg.west.box A client box (optional) * @param {Object} arg.south (optional) An object with the following * members: * @param {int} arg.south.width width of the east area * @param {SUI.Box} arg.south.box A client box (optional) */ initializer: function(arg) { SUI.SplitLayout.initializeBase(this, arg); // add the center this.center = new SUI.Box({parent: this}); if (arg.center && arg.center.box) { this.add(arg.center.box, "center"); } // add north, south, west and east if given in arguments if (arg.north) { this.north = new SUI.Box({parent: this}); this.north.height(arg.north.height); if (arg.north.box) { this.add(arg.north.box, "north"); } } if (arg.south) { this.south = new SUI.Box({parent: this}); this.south.height(arg.south.height); if (arg.south.box) { this.add(arg.south.box, "south"); } } if (arg.west) { this.west = new SUI.Box({parent: this}); this.west.width(arg.west.width); if (arg.west.box) { this.add(arg.west.box, "west"); } } if (arg.east) { this.east = new SUI.Box({parent: this}); this.east.width(arg.east.width); if (arg.east.box) { this.add(arg.east.box, "east"); } } }, /** * {SUI.Box} Center box */ center: null, /** * {SUI.Box} East box */ east: null, /** * {SUI.Box} North box */ north: null, /** * {SUI.Box} South box */ south: null, /** * {SUI.Box} West box */ west: null, /** * Add a child to the current location. You might need an onremove * handler to store the data on the current location if it is replaced * by a new box. To allow for asynchronious usage the remainder of the * function is not executed when the onRemove callback is provided. * One has to finish the action yourself by calling "finishAdd" * which will remove the current element from the frame an replaces * it with the given child. * @param {SUI.Box} child Box to add to the split layout * @param {String} location Location to add the box to ("west", "south", * "north" or "west") * @param {Function} onRemove Function to execue when the child box is * removed from the split layout */ add: function(child, location, onRemove) { // get the requested location var t = this[location]; if (t) { // get the current child ... var loc = this.get(t); // is the location is set and there is an onRemove method ... if (t.onRemove) { // ... set the parameters for a 'delayed add' this._faData = { frame: t, loc: loc, child: child, onRemove: onRemove}; // ... do that method and leave the "finishAdd" to // the implementation of onRemove. ... t.callListener("onRemove", loc); } else { if (loc){ // remove the box at the location this.remove(loc, t); } // ... else just add the child to the location SUI.SplitLayout.parentMethod(this, "add", child, t); this._minsizeCalc(); // store the onRemove handler, need it for the next add // TODO note: don't use addListener t.onRemove = onRemove || null; } } else { throw "Splitlayout: Adding to an inexisting location"; } }, /** * Set the CSS width and height of she SplitLayout, its locations and * all the child boxes. */ display: function() { // set the CSS dimensions of the container box ... this.setDim(); // ... and the of the location boxes ... if (this.north) { this.north.setDim(); } if (this.south) { this.south.setDim(); } if (this.west) { this.west.setDim(); } if (this.east) { this.east.setDim(); } this.center.setDim(); // ... and of all the children SUI.SplitLayout.parentMethod(this, "display"); }, /** * Get the child box attached to the location. * @param {String} location The location (west, east, north or south) for * which to retrieve the child box * @returns {SUI.Box} The requested child box, null if there is none */ get: function(location) { for (var i=0; i<this.children.length; i++) { if (this.children[i].parent() == location) { return this.children[i]; } } return null; }, /** * The framework's layOut method. Set the positions of all the * SplitLayout's location boxes and call the layOut method of all child * boxes */ layOut: function() { // get the minimum widths and heights var tmp = this._prepareLayout(); var w = tmp.w; // corrected widths var h = tmp.h; // corrected heights var ct = 0; // center top var cl = 0; // center left var cw = w.w; // center width var ch = h.h; // center height // set the dimensions of the container box this.setRect(this.top(), this.left(), w.w, h.h); // if there is a north panel ... if (this.north) { // ... set the dimensions ... this.north.setRect(0, 0, w.w, h.nh); // ... and adjust the top and height of the center ct += h.nh; ch -= ct; } // if there is a south panel ... if (this.south) { // ... set the dimensions ... this.south.setRect(h.h-h.sh, 0, w.w, h.sh); // ... and adjust the height of the center ch -= h.sh; } // if there is a west panel ... if (this.west) { // ... set the dimensions ... this.west.setRect(ct, 0, w.ew, ch); // ... and adjust the left and width of the center cl += w.ew; cw -= cl; } // if there is an east panel ... if (this.east) { // ... set the dimensions ... this.east.setRect(ct, w.w-w.ww, w.ww, ch); // ... and adjust the width of the center cw -= w.ww; } // now we know the position of the center this.center.setRect(ct, cl, cw, ch); // layOut the child boxes SUI.SplitLayout.parentMethod(this, "layOut"); return; }, /** * When you use an onRemove handler on add you'll have to finish this * add procedure yourself by calling finishAdd(). This because your * onRemove handler can do asynchronious stuff that is not within the * current thread of control. */ finishAdd: function() { if (this._faData) { // remove the box at the location this.remove(this._faData.loc, this._faData.frame); /// and add the the new one SUI.SplitLayout.parentMethod(this, "add", this._faData.child, this._faData.frame); this._minsizeCalc(); // store the onRemove handler, need it for the next add // TODO note: don't use addListener this._faData.frame.onRemove = this._faData.onRemove || null; this._faData = null; } }, // Width of the border between panels _borderWidth: 0, // Data stored for a delayed add method (when onremove needs data on // the current location) _faData: null, /* recalculate total, north and south height if component is too large */ _correctHeight: function(height) { // north and south height var n = this.north ? this.north.height() : 0; var s = this.south ? this.south.height() : 0; // total height including borders var c = n + s + this.center.minHeight() + (this.north ? this._borderWidth : 0) + (this.south ? this._borderWidth : 0); // recalculate var r = this._correctSizeCalc(height, c, this.minHeight(), n, s, this.north ? this.north.minHeight() : 0, this.south ? this.south.minHeight() : 0, this.north ? true : false, this.south ? true : false); // and return total, north and south height return {h: r.t, nh: r.nw, sh: r.se}; }, /* recalculate total, west and east width if component is too large */ _correctWidth: function(width) { // west and east width var w = this.west ? this.west.width() : 0; var e = this.east ? this.east.width() : 0; // total width including borders var c = w + e + this.center.minWidth() + (this.west ? this._borderWidth : 0) + (this.east ? this._borderWidth : 0); // recalculate var r = this._correctSizeCalc(width, c, this.minWidth(), w, e, this.west ? this.west.minWidth() : 0, this.east ? this.east.minWidth() : 0, this.west ? true : false, this.east ? true : false); // and return total, west and east width return {w: r.t, ew: r.nw, ww: r.se}; }, /* If total w/h (t) is smaller than min w/h (m) set total w/h as * min w/h or if total w/h is smaller than current w/h (c) decrease * west/north w/h (nw) and east/south w/h (se) with proper amount * (mnw/mse: min nw/se, hnw/hse: has nw/se) */ _correctSizeCalc: function(t, c, m, nw, se, mnw, mse, hnw, hse) { if (m > t) { // if total height (or width) (t) is smaller than minimum height // (or width) (m) then ... // ... set north height (or west width) to set to minimum values // if there is a north (or west) panel, ... if (hnw) { nw = mnw; } // ... set south height (or east width) to set to minimum values // if there is a south (or east) panel, ... if (hse) { se = mse; } // ... and set the total height (or width) to the minimum value. t = m; } else if (c > t) { // else if total height (or width) (t) is smaller than current // height (or width) (c) then ... // ... calculate the part that should be subtracted ... var rest = (hnw && hse) ? Math.ceil((c-t)/2) : c-t; // ... and if we have a north (or west) panel ... if (hnw) { // ... try to subtract it from that side ... nw -= rest; // ... but if it is too much and ... if (nw < mnw) { // ... there is an other side ... if (hse) { // ... try to get more from that other side ... rest += (mnw - nw); } // ... and set the north height (or west width) to // the minimum value nw = mnw; } } // ... and if we have a south (or east) panel ... if (hse) { // ... try to subtract it from that side ... se -= rest; // ... but if it is too much and ... if (se < mse) { // ... there is an other side ... if (hnw) { // ... try to get more from that other side ... nw -= (mse - se); // ... not not more than the maximum value ... if (nw < mnw) { nw = mnw; } } // ... and set the south height (or east width) to // the minimum value se = mse; } } } // return the new center north (or west) and south (or east) size return {t:t,nw:nw,se:se}; }, /* Get the mimimum size for the box by checking the minimal sizes of the * client boxes */ _minsizeCalc: function() { // get the minimal center width and height this.minWidth(this.center.minWidth()); this.minHeight(this.center.minHeight()); // if there is a north box add its minimal height if (this.north) { this.minHeight(this.minHeight() + this.north.minHeight() + this._borderWidth); } // if there is a south box add its minimal height if (this.south) { this.minHeight(this.minHeight() + this.south.minHeight() + this._borderWidth); } // if there is a west box add its minimal width if (this.west) { this.minWidth(this.minWidth() + this.west.minWidth() + this._borderWidth); } // if there is a east box add its minimal width if (this.east) { this.minWidth(this.minWidth() + this.east.minWidth() + this._borderWidth); } }, /* Retrieve the minimum and maximum height and width settings of child * boxes (can differ from the ones set in this component). */ _prepareLayout: function() { // for all children ... for (var i=0; i<this.children.length; i++) { var c = this.children[i]; // ... set the parent's min/max width/height to the child's // min/max width/height if appropriate if (c.parent().minWidth() < c.minWidth()) { c.parent().minWidth(c.minWidth()); } if (c.parent().maxWidth() > c.maxWidth()) { c.parent().maxWidth(c.maxWidth()); } if (c.parent().minHeight() < c.minHeight()) { c.parent().minHeight(c.minHeight()); } if (c.parent().maxHeight() > c.maxHeight()) { c.parent().maxHeight(c.maxHeight()); } } // return adjusted width/height if necessary return { w: this._correctWidth(this.width()), h: this._correctHeight(this.height()) }; } });