@qooxdoo/framework
Version:
The JS Framework for Coders
631 lines (518 loc) • 17.1 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Jonathan Weiß (jonathan_rass)
************************************************************************ */
/**
* A split panes divides an area into two panes. The ratio between the two
* panes is configurable by the user using the splitter.
*
* @childControl slider {qx.ui.splitpane.Slider} shown during resizing the splitpane
* @childControl splitter {qx.ui.splitpane.Splitter} splitter to resize the splitpane
*/
qx.Class.define("qx.ui.splitpane.Pane",
{
extend : qx.ui.core.Widget,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* Creates a new instance of a SplitPane. It allows the user to dynamically
* resize the areas dropping the border between.
*
* @param orientation {String} The orientation of the split pane control.
* Allowed values are "horizontal" (default) and "vertical".
*/
construct : function(orientation)
{
this.base(arguments);
this.__children = [];
// Initialize orientation
if (orientation) {
this.setOrientation(orientation);
} else {
this.initOrientation();
}
// add all pointer listener to the blocker
this.__blocker.addListener("pointerdown", this._onPointerDown, this);
this.__blocker.addListener("pointerup", this._onPointerUp, this);
this.__blocker.addListener("pointermove", this._onPointerMove, this);
this.__blocker.addListener("pointerout", this._onPointerOut, this);
this.__blocker.addListener("losecapture", this._onPointerUp, this);
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
// overridden
appearance :
{
refine : true,
init : "splitpane"
},
/**
* Distance between pointer and splitter when the cursor should change
* and enable resizing.
*/
offset :
{
check : "Integer",
init : 6,
apply : "_applyOffset"
},
/**
* The orientation of the splitpane control.
*/
orientation :
{
init : "horizontal",
check : [ "horizontal", "vertical" ],
apply : "_applyOrientation"
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__splitterOffset : null,
__activeDragSession : false,
__lastPointerX : null,
__lastPointerY : null,
__isHorizontal : null,
__beginSize : null,
__endSize : null,
__children : null,
__blocker : null,
// overridden
_createChildControlImpl : function(id, hash)
{
var control;
switch(id)
{
// Create and add slider
case "slider":
control = new qx.ui.splitpane.Slider(this);
control.exclude();
this._add(control, {type : id});
break;
// Create splitter
case "splitter":
control = new qx.ui.splitpane.Splitter(this);
this._add(control, {type : id});
control.addListener("move", this.__onSplitterMove, this);
break;
}
return control || this.base(arguments, id);
},
/**
* Move handler for the splitter which takes care of the external
* triggered resize of children.
*
* @param e {qx.event.type.Data} The data even of move.
*/
__onSplitterMove : function(e) {
this.__setBlockerPosition(e.getData());
},
/**
* Creates a blocker for the splitter which takes all bouse events and
* also handles the offset and cursor.
*
* @param orientation {String} The orientation of the pane.
*/
__createBlocker : function(orientation) {
this.__blocker = new qx.ui.splitpane.Blocker(orientation);
this.getContentElement().add(this.__blocker);
var splitter = this.getChildControl("splitter");
var splitterWidth = splitter.getWidth();
if (!splitterWidth) {
splitter.addListenerOnce("appear", function() {
this.__setBlockerPosition();
}, this);
}
// resize listener to remove the blocker in case the splitter
// is removed.
splitter.addListener("resize", function(e) {
var bounds = e.getData();
if (this.getChildControl("splitter").getVisible() && (bounds.height == 0 || bounds.width == 0)) {
this.__blocker.hide();
} else {
this.__blocker.show();
}
}, this);
},
/**
* Returns the blocker used over the splitter. this could be used for
* adding event listeners like tap or dbltap.
*
* @return {qx.ui.splitpane.Blocker} The used blocker element.
*
* @internal
*/
getBlocker : function() {
return this.__blocker;
},
/*
---------------------------------------------------------------------------
PROPERTY APPLY METHODS
---------------------------------------------------------------------------
*/
/**
* Apply routine for the orientation property.
*
* Sets the pane's layout to vertical or horizontal split layout.
*
* @param value {String} The new value of the orientation property
* @param old {String} The old value of the orientation property
*/
_applyOrientation : function(value, old)
{
var slider = this.getChildControl("slider");
var splitter = this.getChildControl("splitter");
// Store boolean flag for faster access
this.__isHorizontal = value === "horizontal";
if (!this.__blocker) {
this.__createBlocker(value);
}
// update the blocker
this.__blocker.setOrientation(value);
// Dispose old layout
var oldLayout = this._getLayout();
if (oldLayout) {
oldLayout.dispose();
}
// Create new layout
var newLayout = value === "vertical" ?
new qx.ui.splitpane.VLayout : new qx.ui.splitpane.HLayout;
this._setLayout(newLayout);
// Update states for splitter and slider
splitter.removeState(old);
splitter.addState(value);
splitter.getChildControl("knob").removeState(old);
splitter.getChildControl("knob").addState(value);
slider.removeState(old);
slider.addState(value);
// flush (needs to be done for the blocker update) and update the blocker
qx.ui.core.queue.Manager.flush();
this.__setBlockerPosition();
},
// property apply
_applyOffset : function(value, old) {
this.__setBlockerPosition();
},
/**
* Helper for setting the blocker to the right position, which depends on
* the offset, orientation and the current position of the splitter.
*
* @param bounds {Map?null} If the bounds of the splitter are known,
* they can be added.
*/
__setBlockerPosition : function(bounds) {
var splitter = this.getChildControl("splitter");
var offset = this.getOffset();
var splitterBounds = splitter.getBounds();
var splitterElem = splitter.getContentElement().getDomElement();
// do nothing if the splitter is not ready
if (!splitterElem) {
return;
}
// recalculate the dimensions of the blocker
if (this.__isHorizontal) {
// get the width either of the given bounds or of the read bounds
var width = null;
if (bounds) {
width = bounds.width;
} else if (splitterBounds) {
width = splitterBounds.width;
}
var left = bounds && bounds.left;
if (width || !this.getChildControl("splitter").getVisible()) {
if (isNaN(left)) {
left = qx.bom.element.Location.getPosition(splitterElem).left;
}
this.__blocker.setWidth(offset, width || 6);
this.__blocker.setLeft(offset, left);
}
// vertical case
} else {
// get the height either of the given bounds or of the read bounds
var height = null;
if (bounds) {
height = bounds.height;
} else if (splitterBounds) {
height = splitterBounds.height;
}
var top = bounds && bounds.top;
if (height || !this.getChildControl("splitter").getVisible()) {
if (isNaN(top)) {
top = qx.bom.element.Location.getPosition(splitterElem).top;
}
this.__blocker.setHeight(offset, height || 6);
this.__blocker.setTop(offset, top);
}
}
},
/*
---------------------------------------------------------------------------
PUBLIC METHODS
---------------------------------------------------------------------------
*/
/**
* Adds a widget to the pane.
*
* Sets the pane's layout to vertical or horizontal split layout. Depending on the
* pane's layout the first widget will be the left or top widget, the second one
* the bottom or right widget. Adding more than two widgets will overwrite the
* existing ones.
*
* @param widget {qx.ui.core.Widget} The widget to be inserted into pane.
* @param flex {Number} The (optional) layout property for the widget's flex value.
*/
add : function(widget, flex)
{
if (flex == null) {
this._add(widget);
} else {
this._add(widget, {flex : flex});
}
this.__children.push(widget);
},
/**
* Removes the given widget from the pane.
*
* @param widget {qx.ui.core.Widget} The widget to be removed.
*/
remove : function(widget)
{
this._remove(widget);
qx.lang.Array.remove(this.__children, widget);
},
/**
* Returns an array containing the pane's content.
*
* @return {qx.ui.core.Widget[]} The pane's child widgets
*/
getChildren : function() {
return this.__children;
},
/*
---------------------------------------------------------------------------
POINTER LISTENERS
---------------------------------------------------------------------------
*/
/**
* Handler for pointerdown event.
*
* Shows slider widget and starts drag session if pointer is near/on splitter widget.
*
* @param e {qx.event.type.Pointer} pointerdown event
*/
_onPointerDown : function(e)
{
// Only proceed if left pointer button is pressed and the splitter is active
if (!e.isLeftPressed()) {
return;
}
var splitter = this.getChildControl("splitter");
// Store offset between pointer event coordinates and splitter
var splitterLocation = splitter.getContentLocation();
var paneLocation = this.getContentLocation();
this.__splitterOffset = this.__isHorizontal ?
e.getDocumentLeft() - splitterLocation.left + paneLocation.left :
e.getDocumentTop() - splitterLocation.top + paneLocation.top ;
// Synchronize slider to splitter size and show it
var slider = this.getChildControl("slider");
var splitterBounds = splitter.getBounds();
slider.setUserBounds(
splitterBounds.left, splitterBounds.top,
splitterBounds.width || 6, splitterBounds.height || 6
);
slider.setZIndex(splitter.getZIndex() + 1);
slider.show();
// Enable session
this.__activeDragSession = true;
this.__blocker.capture();
e.stop();
},
/**
* Handler for pointermove event.
*
* @param e {qx.event.type.Pointer} pointermove event
*/
_onPointerMove : function(e)
{
this._setLastPointerPosition(e.getDocumentLeft(), e.getDocumentTop());
// Check if slider is already being dragged
if (this.__activeDragSession)
{
// Compute new children sizes
this.__computeSizes();
// Update slider position
var slider = this.getChildControl("slider");
var pos = this.__beginSize;
if(this.__isHorizontal) {
slider.setDomLeft(pos);
this.__blocker.setStyle("left", (pos - this.getOffset()) + "px");
} else {
slider.setDomTop(pos);
this.__blocker.setStyle("top", (pos - this.getOffset()) + "px");
}
e.stop();
}
},
/**
* Handler for pointerout event
*
* @param e {qx.event.type.Pointer} pointerout event
*/
_onPointerOut : function(e)
{
this._setLastPointerPosition(e.getDocumentLeft(), e.getDocumentTop());
},
/**
* Handler for pointerup event
*
* Sets widget sizes if dragging session has been active.
*
* @param e {qx.event.type.Pointer} pointerup event
*/
_onPointerUp : function(e)
{
if (!this.__activeDragSession) {
return;
}
// Set sizes to both widgets
this._finalizeSizes();
// Hide the slider
var slider = this.getChildControl("slider");
slider.exclude();
// Cleanup
this.__activeDragSession = false;
this.releaseCapture();
e.stop();
},
/*
---------------------------------------------------------------------------
INTERVAL HANDLING
---------------------------------------------------------------------------
*/
/**
* Updates widgets' sizes based on the slider position.
*/
_finalizeSizes : function()
{
var beginSize = this.__beginSize;
var endSize = this.__endSize;
if (beginSize == null) {
return;
}
var children = this._getChildren();
var firstWidget = children[2];
var secondWidget = children[3];
// Read widgets' flex values
var firstFlexValue = firstWidget.getLayoutProperties().flex;
var secondFlexValue = secondWidget.getLayoutProperties().flex;
// Both widgets have flex values
if((firstFlexValue != 0) && (secondFlexValue != 0))
{
firstWidget.setLayoutProperties({ flex : beginSize });
secondWidget.setLayoutProperties({ flex : endSize });
}
// Update both sizes
else
{
// Set widths to static widgets
if (this.__isHorizontal)
{
firstWidget.setWidth(beginSize);
secondWidget.setWidth(endSize);
}
else
{
firstWidget.setHeight(beginSize);
secondWidget.setHeight(endSize);
}
}
},
/**
* Computes widgets' sizes based on the pointer coordinate.
*/
__computeSizes : function()
{
if (this.__isHorizontal) {
var min="minWidth", size="width", max="maxWidth", pointer=this.__lastPointerX;
} else {
var min="minHeight", size="height", max="maxHeight", pointer=this.__lastPointerY;
}
var children = this._getChildren();
var beginHint = children[2].getSizeHint();
var endHint = children[3].getSizeHint();
// Area given to both widgets
var allocatedSize = children[2].getBounds()[size] + children[3].getBounds()[size];
// Calculate widget sizes
var beginSize = pointer - this.__splitterOffset;
var endSize = allocatedSize - beginSize;
// Respect minimum limits
if (beginSize < beginHint[min])
{
endSize -= beginHint[min] - beginSize;
beginSize = beginHint[min];
}
else if (endSize < endHint[min])
{
beginSize -= endHint[min] - endSize;
endSize = endHint[min];
}
// Respect maximum limits
if (beginSize > beginHint[max])
{
endSize += beginSize - beginHint[max];
beginSize = beginHint[max];
}
else if (endSize > endHint[max])
{
beginSize += endSize - endHint[max];
endSize = endHint[max];
}
// Store sizes
this.__beginSize = beginSize;
this.__endSize = endSize;
},
/**
* Determines whether this is an active drag session
*
* @return {Boolean} True if active drag session, otherwise false.
*/
_isActiveDragSession : function() {
return this.__activeDragSession;
},
/**
* Sets the last pointer position.
*
* @param x {Integer} the x position of the pointer.
* @param y {Integer} the y position of the pointer.
*/
_setLastPointerPosition : function(x, y)
{
this.__lastPointerX = x;
this.__lastPointerY = y;
}
},
destruct : function() {
this.__children = null;
}
});