/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2006 by STZ-IDA, Germany, http://www.stz-ida.de
License:
LGPL 2.1: http://www.gnu.org/licenses/lgpl.html
Authors:
* Til Schneider (til132)
************************************************************************ */
/* ************************************************************************
#module(ui_table)
************************************************************************ */
/**
* Shows a whole meta column. This includes a {@link TablePaneHeader},
* a {@link TablePane} and the needed scroll bars. This class handles the
* virtual scrolling and does all the mouse event handling.
*
* @param table {Table} the table the scroller belongs to.
*/
qx.OO.defineClass("qx.ui.table.TablePaneScroller", qx.ui.layout.VerticalBoxLayout,
function(table) {
qx.ui.layout.VerticalBoxLayout.call(this);
this._table = table;
// init scrollbars
this._verScrollBar = new qx.ui.core.ScrollBar(false);
this._horScrollBar = new qx.ui.core.ScrollBar(true);
var scrollBarWidth = this._verScrollBar.getPreferredBoxWidth();
this._verScrollBar.setWidth("auto");
this._horScrollBar.setHeight("auto");
this._horScrollBar.setPaddingRight(scrollBarWidth);
//this._verScrollBar.setMergeEvents(true);
this._horScrollBar.addEventListener("changeValue", this._onScrollX, this);
this._verScrollBar.addEventListener("changeValue", this._onScrollY, this);
// init header
this._header = new qx.ui.table.TablePaneHeader(this);
this._header.set({ width:"auto", height:"auto" });
this._headerClipper = new qx.ui.layout.CanvasLayout;
with (this._headerClipper) {
setDimension("1*", "auto");
setOverflow("hidden");
add(this._header);
}
this._spacer = new qx.ui.basic.Terminator;
this._spacer.setWidth(scrollBarWidth);
this._top = new qx.ui.layout.HorizontalBoxLayout;
with (this._top) {
setHeight("auto");
add(this._headerClipper, this._spacer);
}
// init pane
this._tablePane = new qx.ui.table.TablePane(this);
this._tablePane.set({ width:"auto", height:"auto" });
this._focusIndicator = new qx.ui.layout.HorizontalBoxLayout;
this._focusIndicator.setAppearance("table-focus-indicator");
this._focusIndicator.hide();
// Workaround: If the _focusIndicator has no content if always gets a too
// high hight in IE.
var dummyContent = new qx.ui.basic.Terminator;
dummyContent.setWidth(0);
this._focusIndicator.add(dummyContent);
this._paneClipper = new qx.ui.layout.CanvasLayout;
with (this._paneClipper) {
setWidth("1*");
setOverflow("hidden");
add(this._tablePane, this._focusIndicator);
addEventListener("mousewheel", this._onmousewheel, this);
}
// add all child widgets
var scrollerBody = new qx.ui.layout.HorizontalBoxLayout;
scrollerBody.setHeight("1*");
scrollerBody.add(this._paneClipper, this._verScrollBar);
this.add(this._top, scrollerBody, this._horScrollBar);
// init event handlers
this.addEventListener("mousemove", this._onmousemove, this);
this.addEventListener("mousedown", this._onmousedown, this);
this.addEventListener("mouseup", this._onmouseup, this);
this.addEventListener("click", this._onclick, this);
this.addEventListener("dblclick", this._ondblclick, this);
this.addEventListener("mouseout", this._onmouseout, this);
});
/** Whether to show the horizontal scroll bar */
qx.OO.addProperty({ name:"horizontalScrollBarVisible", type:"boolean", defaultValue:true });
/** Whether to show the vertical scroll bar */
qx.OO.addProperty({ name:"verticalScrollBarVisible", type:"boolean", defaultValue:true });
/** The table pane model. */
qx.OO.addProperty({ name:"tablePaneModel", type:"object", instance:"qx.ui.table.TablePaneModel" });
/** The current position of the the horizontal scroll bar. */
qx.OO.addProperty({ name:"scrollX", type:"number", allowNull:false, defaultValue:0 });
/** The current position of the the vertical scroll bar. */
qx.OO.addProperty({ name:"scrollY", type:"number", allowNull:false, defaultValue:0 });
/**
* Whether column resize should be live. If false, during resize only a line is
* shown and the real resize happens when the user releases the mouse button.
*/
qx.OO.addProperty({ name:"liveResize", type:"boolean", defaultValue:false });
/**
* Whether the focus should moved when the mouse is moved over a cell. If false
* the focus is only moved on mouse clicks.
*/
qx.OO.addProperty({ name:"focusCellOnMouseMove", type:"boolean", defaultValue:false });
// property modifier
qx.Proto._modifyHorizontalScrollBarVisible = function(propValue, propOldValue, propData) {
// Workaround: We can't use setDisplay, because the scroll bar needs its
// correct height in order to check its value. When using
// setDisplay(false) the height isn't relayouted any more
if (propValue) {
this._horScrollBar.setHeight("auto");
} else {
this._horScrollBar.setHeight(0);
}
this._horScrollBar.setVisibility(propValue);
// NOTE: We have to flush the queues before updating the content so the new
// layout has been applied and _updateContent is able to work with
// correct values.
qx.ui.core.Widget.flushGlobalQueues();
this._updateContent();
return true;
}
// property modifier
qx.Proto._modifyVerticalScrollBarVisible = function(propValue, propOldValue, propData) {
// Workaround: See _modifyHorizontalScrollBarVisible
if (propValue) {
this._verScrollBar.setWidth("auto");
} else {
this._verScrollBar.setWidth(0);
}
this._verScrollBar.setVisibility(propValue);
var scrollBarWidth = propValue ? this._verScrollBar.getPreferredBoxWidth() : 0;
this._horScrollBar.setPaddingRight(scrollBarWidth);
this._spacer.setWidth(scrollBarWidth);
return true;
}
// property modifier
qx.Proto._modifyTablePaneModel = function(propValue, propOldValue, propData) {
if (propOldValue != null) {
propOldValue.removeEventListener("modelChanged", this._onPaneModelChanged, this);
}
propValue.addEventListener("modelChanged", this._onPaneModelChanged, this);
return true;
}
// property modifier
qx.Proto._modifyScrollX = function(propValue, propOldValue, propData) {
this._horScrollBar.setValue(propValue);
return true;
}
// property modifier
qx.Proto._modifyScrollY = function(propValue, propOldValue, propData) {
this._verScrollBar.setValue(propValue);
return true;
}
/**
* Returns the table this scroller belongs to.
*
* @return {Table} the table.
*/
qx.Proto.getTable = function() {
return this._table;
};
/**
* Event handler. Called when the visibility of a column has changed.
*
* @param evt {Map} the event.
*/
qx.Proto._onColVisibilityChanged = function(evt) {
this._updateHorScrollBarMaximum();
this._updateFocusIndicator();
}
/**
* Event handler. Called when the width of a column has changed.
*
* @param evt {Map} the event.
*/
qx.Proto._onColWidthChanged = function(evt) {
this._header._onColWidthChanged(evt);
this._tablePane._onColWidthChanged(evt);
var data = evt.getData();
var paneModel = this.getTablePaneModel();
var x = paneModel.getX(data.col);
if (x != -1) {
// The change was in this scroller
this._updateHorScrollBarMaximum();
this._updateFocusIndicator();
}
}
/**
* Event handler. Called when the column order has changed.
*
* @param evt {Map} the event.
*/
qx.Proto._onColOrderChanged = function(evt) {
this._header._onColOrderChanged(evt);
this._tablePane._onColOrderChanged(evt);
this._updateHorScrollBarMaximum();
}
/**
* Event handler. Called when the table model has changed.
*
* @param evt {Map} the event.
*/
qx.Proto._onTableModelDataChanged = function(evt) {
this._tablePane._onTableModelDataChanged(evt);
var rowCount = this.getTable().getTableModel().getRowCount();
if (rowCount != this._lastRowCount) {
this._lastRowCount = rowCount;
this._updateVerScrollBarMaximum();
if (this.getFocusedRow() >= rowCount) {
if (rowCount == 0) {
this.setFocusedCell(null, null);
} else {
this.setFocusedCell(this.getFocusedColumn(), rowCount - 1);
}
}
}
}
/**
* Event handler. Called when the selection has changed.
*
* @param evt {Map} the event.
*/
qx.Proto._onSelectionChanged = function(evt) {
this._tablePane._onSelectionChanged(evt);
};
/**
* Event handler. Called when the table gets or looses the focus.
*/
qx.Proto._onFocusChanged = function(evt) {
this._focusIndicator.setState("tableHasFocus", this.getTable().getFocused());
this._tablePane._onFocusChanged(evt);
};
/**
* Event handler. Called when the table model meta data has changed.
*
* @param evt {Map} the event.
*/
qx.Proto._onTableModelMetaDataChanged = function(evt) {
this._header._onTableModelMetaDataChanged(evt);
this._tablePane._onTableModelMetaDataChanged(evt);
};
/**
* Event handler. Called when the pane model has changed.
*
* @param evt {Map} the event.
*/
qx.Proto._onPaneModelChanged = function(evt) {
this._header._onPaneModelChanged(evt);
this._tablePane._onPaneModelChanged(evt);
};
/**
* Updates the maximum of the horizontal scroll bar, so it corresponds to the
* total width of the columns in the table pane.
*/
qx.Proto._updateHorScrollBarMaximum = function() {
this._horScrollBar.setMaximum(this.getTablePaneModel().getTotalWidth());
}
/**
* Updates the maximum of the vertical scroll bar, so it corresponds to the
* number of rows in the table.
*/
qx.Proto._updateVerScrollBarMaximum = function() {
var rowCount = this.getTable().getTableModel().getRowCount();
var rowHeight = this.getTable().getRowHeight();
if (this.getTable().getKeepFirstVisibleRowComplete()) {
this._verScrollBar.setMaximum((rowCount + 1) * rowHeight);
} else {
this._verScrollBar.setMaximum(rowCount * rowHeight);
}
}
/**
* Event handler. Called when the table property "keepFirstVisibleRowComplete"
* changed.
*/
qx.Proto._onKeepFirstVisibleRowCompleteChanged = function() {
this._updateVerScrollBarMaximum();
this._updateContent();
};
// overridden
qx.Proto._changeInnerHeight = function(newValue, oldValue) {
// The height has changed -> Update content
this._postponedUpdateContent();
return qx.ui.layout.VerticalBoxLayout.prototype._changeInnerHeight.call(this, newValue, oldValue);
}
// overridden
qx.Proto._afterAppear = function() {
qx.ui.layout.VerticalBoxLayout.prototype._afterAppear.call(this);
var self = this;
this.getElement().onselectstart = qx.util.Return.returnFalse;
this._updateContent();
this._header._updateContent();
this._updateHorScrollBarMaximum();
this._updateVerScrollBarMaximum();
}
/**
* Event handler. Called when the horizontal scroll bar moved.
*
* @param evt {Map} the event.
*/
qx.Proto._onScrollX = function(evt) {
// Workaround: See _updateContent
this._header.setLeft(-evt.getData());
this._paneClipper.setScrollLeft(evt.getData());
this.setScrollX(evt.getData());
}
/**
* Event handler. Called when the vertical scroll bar moved.
*
* @param evt {Map} the event.
*/
qx.Proto._onScrollY = function(evt) {
this._postponedUpdateContent();
this.setScrollY(evt.getData());
}
/**
* Event handler. Called when the user moved the mouse wheel.
*
* @param evt {Map} the event.
*/
qx.Proto._onmousewheel = function(evt) {
this._verScrollBar.setValue(this._verScrollBar.getValue()
- evt.getWheelDelta() * this.getTable().getRowHeight());
// Update the focus
if (this._lastMousePageX && this.getFocusCellOnMouseMove()) {
this._focusCellAtPagePos(this._lastMousePageX, this._lastMousePageY);
}
}
/**
* Event handler. Called when the user moved the mouse.
*
* @param evt {Map} the event.
*/
qx.Proto._onmousemove = function(evt) {
var tableModel = this.getTable().getTableModel();
var columnModel = this.getTable().getTableColumnModel();
var useResizeCursor = false;
var mouseOverColumn = null;
var pageX = evt.getPageX();
var pageY = evt.getPageY();
// Workaround: In onmousewheel the event has wrong coordinates for pageX
// and pageY. So we remember the last move event.
this._lastMousePageX = pageX;
this._lastMousePageY = pageY;
if (this._resizeColumn != null) {
// We are currently resizing -> Update the position
var minColumnWidth = qx.ui.table.TablePaneScroller.MIN_COLUMN_WIDTH;
var newWidth = Math.max(minColumnWidth, this._lastResizeWidth + pageX - this._lastResizeMousePageX);
if (this.getLiveResize()) {
columnModel.setColumnWidth(this._resizeColumn, newWidth);
} else {
this._header.setColumnWidth(this._resizeColumn, newWidth);
var paneModel = this.getTablePaneModel();
this._showResizeLine(paneModel.getColumnLeft(this._resizeColumn) + newWidth);
}
useResizeCursor = true;
this._lastResizeMousePageX += newWidth - this._lastResizeWidth;
this._lastResizeWidth = newWidth;
} else if (this._moveColumn != null) {
// We are moving a column
// Check whether we moved outside the click tolerance so we can start
// showing the column move feedback
// (showing the column move feedback prevents the onclick event)
var clickTolerance = qx.ui.table.TablePaneScroller.CLICK_TOLERANCE;
if (this._header.isShowingColumnMoveFeedback()
|| pageX > this._lastMoveMousePageX + clickTolerance
|| pageX < this._lastMoveMousePageX - clickTolerance)
{
this._lastMoveColPos += pageX - this._lastMoveMousePageX;
this._header.showColumnMoveFeedback(this._moveColumn, this._lastMoveColPos);
// Get the responsible scroller
var targetScroller = this._table.getTablePaneScrollerAtPageX(pageX);
if (this._lastMoveTargetScroller && this._lastMoveTargetScroller != targetScroller) {
this._lastMoveTargetScroller.hideColumnMoveFeedback();
}
if (targetScroller != null) {
this._lastMoveTargetX = targetScroller.showColumnMoveFeedback(pageX);
} else {
this._lastMoveTargetX = null;
}
this._lastMoveTargetScroller = targetScroller;
this._lastMoveMousePageX = pageX;
}
} else {
// This is a normal mouse move
var row = this._getRowForPagePos(pageX, pageY);
if (row == -1) {
// The mouse is over the header
var resizeCol = this._getResizeColumnForPageX(pageX);
if (resizeCol != -1) {
// The mouse is over a resize region -> Show the right cursor
useResizeCursor = true;
} else {
var col = this._getColumnForPageX(pageX);
if (col != null && tableModel.isColumnSortable(col)) {
mouseOverColumn = col;
}
}
} else if (row != null) {
// The mouse is over the data -> update the focus
if (this.getFocusCellOnMouseMove()) {
this._focusCellAtPagePos(pageX, pageY);
}
}
}
// Workaround: Setting the cursor to the right widget doesn't work
//this._header.setCursor(useResizeCursor ? "e-resize" : null);
this.getTopLevelWidget().setGlobalCursor(useResizeCursor ? qx.ui.table.TablePaneScroller.CURSOR_RESIZE_HORIZONTAL : null);
this._header.setMouseOverColumn(mouseOverColumn);
}
/**
* Event handler. Called when the user pressed a mouse button.
*
* @param evt {Map} the event.
*/
qx.Proto._onmousedown = function(evt) {
var tableModel = this.getTable().getTableModel();
var columnModel = this.getTable().getTableColumnModel();
var pageX = evt.getPageX();
var pageY = evt.getPageY();
var row = this._getRowForPagePos(pageX, pageY);
if (row == -1) {
// mouse is in header
var resizeCol = this._getResizeColumnForPageX(pageX);
if (resizeCol != -1) {
// The mouse is over a resize region -> Start resizing
this._resizeColumn = resizeCol;
this._lastResizeMousePageX = pageX;
this._lastResizeWidth = columnModel.getColumnWidth(this._resizeColumn);
this.setCapture(true);
} else {
// The mouse is not in a resize region
var col = this._getColumnForPageX(pageX);
if (col != null) {
// Prepare column moving
this._moveColumn = col;
this._lastMoveMousePageX = pageX;
this._lastMoveColPos = this.getTablePaneModel().getColumnLeft(col);
this.setCapture(true);
}
}
} else if (row != null) {
// The mouse is over the data -> update the focus
if (! this.getFocusCellOnMouseMove()) {
this._focusCellAtPagePos(pageX, pageY);
}
this.getTable()._getSelectionManager().handleMouseDown(row, evt);
}
}
/**
* Event handler. Called when the user released a mouse button.
*
* @param evt {Map} the event.
*/
qx.Proto._onmouseup = function(evt) {
var columnModel = this.getTable().getTableColumnModel();
var paneModel = this.getTablePaneModel();
if (this._resizeColumn != null) {
// We are currently resizing -> Finish resizing
if (! this.getLiveResize()) {
this._hideResizeLine();
columnModel.setColumnWidth(this._resizeColumn, this._lastResizeWidth);
}
this._resizeColumn = null;
this.setCapture(false);
this.getTopLevelWidget().setGlobalCursor(null);
} else if (this._moveColumn != null) {
// We are moving a column -> Drop the column
this._header.hideColumnMoveFeedback();
if (this._lastMoveTargetScroller) {
this._lastMoveTargetScroller.hideColumnMoveFeedback();
}
if (this._lastMoveTargetX != null) {
var fromVisXPos = paneModel.getFirstColumnX() + paneModel.getX(this._moveColumn);
var toVisXPos = this._lastMoveTargetX;
if (toVisXPos != fromVisXPos && toVisXPos != fromVisXPos + 1) {
// The column was really moved to another position
// (and not moved before or after itself, which is a noop)
// Translate visible positions to overall positions
var fromCol = columnModel.getVisibleColumnAtX(fromVisXPos);
var toCol = columnModel.getVisibleColumnAtX(toVisXPos);
var fromOverXPos = columnModel.getOverallX(fromCol);
var toOverXPos = (toCol != null) ? columnModel.getOverallX(toCol) : columnModel.getOverallColumnCount();
if (toOverXPos > fromOverXPos) {
// Don't count the column itself
toOverXPos--;
}
// Move the column
columnModel.moveColumn(fromOverXPos, toOverXPos);
}
}
this._moveColumn = null;
this._lastMoveTargetX = null;
this.setCapture(false);
} else {
// This is a normal mouse up
var row = this._getRowForPagePos(evt.getPageX(), evt.getPageY());
if (row != -1 && row != null) {
this.getTable()._getSelectionManager().handleMouseUp(row, evt);
}
}
}
/**
* Event handler. Called when the user clicked a mouse button.
*
* @param evt {Map} the event.
*/
qx.Proto._onclick = function(evt) {
var tableModel = this.getTable().getTableModel();
var pageX = evt.getPageX();
var pageY = evt.getPageY();
var row = this._getRowForPagePos(pageX, pageY);
if (row == -1) {
// mouse is in header
var resizeCol = this._getResizeColumnForPageX(pageX);
if (resizeCol == -1) {
// mouse is not in a resize region
var col = this._getColumnForPageX(pageX);
if (col != null && tableModel.isColumnSortable(col)) {
// Sort that column
var sortCol = tableModel.getSortColumnIndex();
var ascending = (col != sortCol) ? true : !tableModel.isSortAscending();
tableModel.sortByColumn(col, ascending);
this.getTable().getSelectionModel().clearSelection();
}
}
} else if (row != null) {
this.getTable()._getSelectionManager().handleClick(row, evt);
}
}
/**
* Event handler. Called when the user double clicked a mouse button.
*
* @param evt {Map} the event.
*/
qx.Proto._ondblclick = function(evt) {
if (! this.isEditing()) {
this._focusCellAtPagePos(evt.getPageX(), evt.getPageY());
this.startEditing();
}
}
/**
* Event handler. Called when the mouse moved out.
*
* @param evt {Map} the event.
*/
qx.Proto._onmouseout = function(evt) {
/*
// Workaround: See _onmousemove
this._lastMousePageX = null;
this._lastMousePageY = null;
*/
// Reset the resize cursor when the mouse leaves the header
// If currently a column is resized then do nothing
// (the cursor will be reset on mouseup)
if (this._resizeColumn == null) {
this.getTopLevelWidget().setGlobalCursor(null);
}
this._header.setMouseOverColumn(null);
}
/**
* Shows the resize line.
*
* @param x {int} the position where to show the line (in pixels, relative to
* the left side of the pane).
*/
qx.Proto._showResizeLine = function(x) {
var resizeLine = this._resizeLine;
if (resizeLine == null) {
resizeLine = new qx.ui.basic.Terminator;
resizeLine.setBackgroundColor("#D6D5D9");
resizeLine.setWidth(3);
this._paneClipper.add(resizeLine);
qx.ui.core.Widget.flushGlobalQueues();
this._resizeLine = resizeLine;
}
resizeLine._applyRuntimeLeft(x - 2); // -1 for the width
resizeLine._applyRuntimeHeight(this._paneClipper.getBoxHeight() + this._paneClipper.getScrollTop());
this._resizeLine.removeStyleProperty("visibility");
}
/**
* Hides the resize line.
*/
qx.Proto._hideResizeLine = function() {
this._resizeLine.setStyleProperty("visibility", "hidden");
}
/**
* Shows the feedback shown while a column is moved by the user.
*
* @param pageX {int} the x position of the mouse in the page (in pixels).
* @return {int} the visible x position of the column in the whole table.
*/
qx.Proto.showColumnMoveFeedback = function(pageX) {
var paneModel = this.getTablePaneModel();
var columnModel = this.getTable().getTableColumnModel();
var paneLeftX = qx.dom.Location.getClientBoxLeft(this._tablePane.getElement());
var colCount = paneModel.getColumnCount();
var targetXPos = 0;
var targetX = 0;
var currX = paneLeftX;
for (var xPos = 0; xPos < colCount; xPos++) {
var col = paneModel.getColumnAtX(xPos);
var colWidth = columnModel.getColumnWidth(col);
if (pageX < currX + colWidth / 2) {
break;
}
currX += colWidth;
targetXPos = xPos + 1;
targetX = currX - paneLeftX;
}
// Ensure targetX is visible
var clipperLeftX = qx.dom.Location.getClientBoxLeft(this._paneClipper.getElement());
var clipperWidth = this._paneClipper.getBoxWidth();
var scrollX = clipperLeftX - paneLeftX;
// NOTE: +2/-1 because of feedback width
targetX = qx.lang.Number.limit(targetX, scrollX + 2, scrollX + clipperWidth - 1);
this._showResizeLine(targetX);
// Return the overall target x position
return paneModel.getFirstColumnX() + targetXPos;
}
/**
* Hides the feedback shown while a column is moved by the user.
*/
qx.Proto.hideColumnMoveFeedback = function() {
this._hideResizeLine();
}
/**
* Sets the focus to the cell that's located at the page position
* pageX
/pageY
. If there is no cell at that position,
* nothing happens.
*
* @param pageX {int} the x position in the page (in pixels).
* @param pageY {int} the y position in the page (in pixels).
*/
qx.Proto._focusCellAtPagePos = function(pageX, pageY) {
var row = this._getRowForPagePos(pageX, pageY);
if (row != -1 && row != null) {
// The mouse is over the data -> update the focus
var col = this._getColumnForPageX(pageX);
if (col != null) {
this._table.setFocusedCell(col, row);
}
}
}
/**
* Sets the currently focused cell.
*
* @param col {int} the model index of the focused cell's column.
* @param row {int} the model index of the focused cell's row.
*/
qx.Proto.setFocusedCell = function(col, row) {
if (!this.isEditing()) {
this._tablePane.setFocusedCell(col, row, this._updateContentPlanned);
this._focusedCol = col;
this._focusedRow = row;
// Move the focus indicator
if (! this._updateContentPlanned) {
this._updateFocusIndicator();
}
}
}
/**
* Returns the column of currently focused cell.
*
* @return {int} the model index of the focused cell's column.
*/
qx.Proto.getFocusedColumn = function() {
return this._focusedCol;
};
/**
* Returns the row of currently focused cell.
*
* @return {int} the model index of the focused cell's column.
*/
qx.Proto.getFocusedRow = function() {
return this._focusedRow;
};
/**
* Scrolls a cell visible.
*
* @param col {int} the model index of the column the cell belongs to.
* @param row {int} the model index of the row the cell belongs to.
*/
qx.Proto.scrollCellVisible = function(col, row) {
var paneModel = this.getTablePaneModel();
var xPos = paneModel.getX(col);
if (xPos != -1) {
var columnModel = this.getTable().getTableColumnModel();
var colLeft = paneModel.getColumnLeft(col);
var colWidth = columnModel.getColumnWidth(col);
var rowHeight = this.getTable().getRowHeight();
var rowTop = row * rowHeight;
var scrollX = this.getScrollX();
var scrollY = this.getScrollY();
var viewWidth = this._paneClipper.getBoxWidth();
var viewHeight = this._paneClipper.getBoxHeight();
// NOTE: We don't use qx.lang.Number.limit, because min should win if max < min
var minScrollX = Math.min(colLeft, colLeft + colWidth - viewWidth);
var maxScrollX = colLeft;
this.setScrollX(Math.max(minScrollX, Math.min(maxScrollX, scrollX)));
var minScrollY = rowTop + rowHeight - viewHeight;
if (this.getTable().getKeepFirstVisibleRowComplete()) {
minScrollY += rowHeight - 1;
}
var maxScrollY = rowTop;
this.setScrollY(Math.max(minScrollY, Math.min(maxScrollY, scrollY)));
}
}
/**
* Returns whether currently a cell is editing.
*
* @return whether currently a cell is editing.
*/
qx.Proto.isEditing = function() {
return this._cellEditor != null;
}
/**
* Starts editing the currently focused cell. Does nothing if already editing
* or if the column is not editable.
*
* @return {boolean} whether editing was started
*/
qx.Proto.startEditing = function() {
var tableModel = this.getTable().getTableModel();
var col = this._focusedCol;
if (!this.isEditing() && (col != null) && tableModel.isColumnEditable(col)) {
var row = this._focusedRow;
var xPos = this.getTablePaneModel().getX(col);
var value = tableModel.getValue(col, row);
this._cellEditorFactory = this.getTable().getTableColumnModel().getCellEditorFactory(col);
var cellInfo = { col:col, row:row, xPos:xPos, value:value }
this._cellEditor = this._cellEditorFactory.createCellEditor(cellInfo);
this._cellEditor.set({ width:"100%", height:"100%" });
this._focusIndicator.add(this._cellEditor);
this._focusIndicator.addState("editing");
this._cellEditor.addEventListener("changeFocused", this._onCellEditorFocusChanged, this);
// Workaround: Calling focus() directly has no effect
var editor = this._cellEditor;
window.setTimeout(function() {
editor.focus();
}, 0);
return true;
}
return false;
}
/**
* Stops editing and writes the editor's value to the model.
*/
qx.Proto.stopEditing = function() {
this.flushEditor();
this.cancelEditing();
}
/**
* Writes the editor's value to the model.
*/
qx.Proto.flushEditor = function() {
if (this.isEditing()) {
var value = this._cellEditorFactory.getCellEditorValue(this._cellEditor);
this.getTable().getTableModel().setValue(this._focusedCol, this._focusedRow, value);
this._table.focus();
}
}
/**
* Stops editing without writing the editor's value to the model.
*/
qx.Proto.cancelEditing = function() {
if (this.isEditing()) {
this._focusIndicator.remove(this._cellEditor);
this._focusIndicator.removeState("editing");
this._cellEditor.dispose();
this._cellEditor.removeEventListener("changeFocused", this._onCellEditorFocusChanged, this);
this._cellEditor = null;
this._cellEditorFactory = null;
}
}
/**
* Event handler. Called when the focused state of the cell editor changed.
*
* @param evt {Map} the event.
*/
qx.Proto._onCellEditorFocusChanged = function(evt) {
if (!this._cellEditor.getFocused()) {
this.stopEditing();
}
}
/**
* Returns the model index of the column the mouse is over or null if the mouse
* is not over a column.
*
* @param pageX {int} the x position of the mouse in the page (in pixels).
* @return {int} the model index of the column the mouse is over.
*/
qx.Proto._getColumnForPageX = function(pageX) {
var headerLeftX = qx.dom.Location.getClientBoxLeft(this._header.getElement());
var columnModel = this.getTable().getTableColumnModel();
var paneModel = this.getTablePaneModel();
var colCount = paneModel.getColumnCount();
var currX = headerLeftX;
for (var x = 0; x < colCount; x++) {
var col = paneModel.getColumnAtX(x);
var colWidth = columnModel.getColumnWidth(col);
currX += colWidth;
if (pageX < currX) {
return col;
}
}
return null;
}
/**
* Returns the model index of the column that should be resized when dragging
* starts here. Returns -1 if the mouse is in no resize region of any column.
*
* @param pageX {int} the x position of the mouse in the page (in pixels).
* @return {int} the column index.
*/
qx.Proto._getResizeColumnForPageX = function(pageX) {
var headerLeftX = qx.dom.Location.getClientBoxLeft(this._header.getElement());
var columnModel = this.getTable().getTableColumnModel();
var paneModel = this.getTablePaneModel();
var colCount = paneModel.getColumnCount();
var currX = headerLeftX;
var regionRadius = qx.ui.table.TablePaneScroller.RESIZE_REGION_RADIUS;
for (var x = 0; x < colCount; x++) {
var col = paneModel.getColumnAtX(x);
var colWidth = columnModel.getColumnWidth(col);
currX += colWidth;
if (pageX >= (currX - regionRadius) && pageX <= (currX + regionRadius)) {
return col;
}
}
return -1;
}
/**
* Returns the model index of the row the mouse is currently over. Returns -1 if
* the mouse is over the header. Returns null if the mouse is not over any
* column.
*
* @param pageX {int} the mouse x position in the page.
* @param pageY {int} the mouse y position in the page.
* @return {int} the model index of the row the mouse is currently over.
*/
qx.Proto._getRowForPagePos = function(pageX, pageY) {
var paneClipperElem = this._paneClipper.getElement();
var paneClipperLeftX = qx.dom.Location.getClientBoxLeft(paneClipperElem);
var paneClipperRightX = qx.dom.Location.getClientBoxRight(paneClipperElem);
if (pageX < paneClipperLeftX || pageX > paneClipperRightX) {
// There was no cell or header cell hit
return null;
}
var paneClipperTopY = qx.dom.Location.getClientBoxTop(paneClipperElem);
var paneClipperBottomY = qx.dom.Location.getClientBoxBottom(paneClipperElem);
if (pageY >= paneClipperTopY && pageY <= paneClipperBottomY) {
// This event is in the pane -> Get the row
var rowHeight = this.getTable().getRowHeight();
var scrollY = this._verScrollBar.getValue();
if (this.getTable().getKeepFirstVisibleRowComplete()) {
scrollY = Math.floor(scrollY / rowHeight) * rowHeight;
}
var tableY = scrollY + pageY - paneClipperTopY;
var row = Math.floor(tableY / rowHeight);
var rowCount = this.getTable().getTableModel().getRowCount();
return (row < rowCount) ? row : null;
}
var headerElem = this._headerClipper.getElement();
if (pageY >= qx.dom.Location.getClientBoxTop(headerElem)
&& pageY <= qx.dom.Location.getClientBoxBottom(headerElem)
&& pageX <= qx.dom.Location.getClientBoxRight(headerElem))
{
// This event is in the pane -> Return -1 for the header
return -1;
}
return null;
}
/**
* Sets the widget that should be shown in the top right corner.
*
* The widget will not be disposed, when this table scroller is disposed. So the * caller has to dispose it. * * @param widget {qx.ui.core.Widget} The widget to set. May be null. */ qx.Proto.setTopRightWidget = function(widget) { var oldWidget = this._topRightWidget; if (oldWidget != null) { this._top.remove(oldWidget); } if (widget != null) { this._top.remove(this._spacer); this._top.add(widget); } else if (oldWidget != null) { this._top.add(this._spacer); } this._topRightWidget = widget; } /** * Returns the header. * * @return {TablePaneHeader} the header. */ qx.Proto.getHeader = function() { return this._header; } /** * Returns the table pane. * * @return {TablePane} the table pane. */ qx.Proto.getTablePane = function() { return this._tablePane; } /** * Returns which scrollbars are needed. * * @param forceHorizontal {boolean ? false} Whether to show the horizontal * scrollbar always. * @param preventVertical {boolean ? false} Whether tp show the vertical scrollbar * never. * @return {int} which scrollbars are needed. This may be any combination of * {@link #HORIZONTAL_SCROLLBAR} or {@link #VERTICAL_SCROLLBAR} * (combined by OR). */ qx.Proto.getNeededScrollBars = function(forceHorizontal, preventVertical) { var barWidth = this._verScrollBar.getPreferredBoxWidth(); // Get the width and height of the view (without scroll bars) var viewWidth = this._paneClipper.getInnerWidth(); if (this.getVerticalScrollBarVisible()) { viewWidth += barWidth; } var viewHeight = this._paneClipper.getInnerHeight(); if (this.getHorizontalScrollBarVisible()) { viewHeight += barWidth; } // Get the (virtual) width and height of the pane var paneWidth = this.getTablePaneModel().getTotalWidth(); var paneHeight = this.getTable().getRowHeight() * this.getTable().getTableModel().getRowCount(); // Check which scrollbars are needed var horNeeded = false; var verNeeded = false; if (paneWidth > viewWidth) { horNeeded = true; if (paneHeight > viewHeight - barWidth) { verNeeded = true; } } else if (paneHeight > viewHeight) { verNeeded = true; if (!preventVertical && (paneWidth > viewWidth - barWidth)) { horNeeded = true; } } // Create the mask var horBar = qx.ui.table.TablePaneScroller.HORIZONTAL_SCROLLBAR; var verBar = qx.ui.table.TablePaneScroller.VERTICAL_SCROLLBAR; return ((forceHorizontal || horNeeded) ? horBar : 0) | ((preventVertical || !verNeeded) ? 0 : verBar); } /** * Does a postponed update of the content. * * @see #_updateContent */ qx.Proto._postponedUpdateContent = function() { if (! this._updateContentPlanned) { var self = this; window.setTimeout(function() { self._updateContent(); self._updateContentPlanned = false; qx.ui.core.Widget.flushGlobalQueues(); }, 0); this._updateContentPlanned = true; } } /** * Updates the content. Sets the right section the table pane should show and * does the scrolling. */ qx.Proto._updateContent = function() { var paneHeight = this._paneClipper.getInnerHeight(); var scrollX = this._horScrollBar.getValue(); var scrollY = this._verScrollBar.getValue(); var rowHeight = this.getTable().getRowHeight(); var firstRow = Math.floor(scrollY / rowHeight); var oldFirstRow = this._tablePane.getFirstVisibleRow(); this._tablePane.setFirstVisibleRow(firstRow); var rowCount = Math.ceil(paneHeight / rowHeight); var paneOffset = 0; if (! this.getTable().getKeepFirstVisibleRowComplete()) { // NOTE: We don't consider paneOffset, because this may cause alternating // adding and deleting of one row when scolling. Instead we add one row // in every case. rowCount++; paneOffset = scrollY % rowHeight; } this._tablePane.setVisibleRowCount(rowCount); if (firstRow != oldFirstRow) { this._updateFocusIndicator(); } // Workaround: We can't use scrollLeft for the header because IE // automatically scrolls the header back, when a column is // resized. this._header.setLeft(-scrollX); this._paneClipper.setScrollLeft(scrollX); this._paneClipper.setScrollTop(paneOffset); //this.debug("paneHeight:"+paneHeight+",rowHeight:"+rowHeight+",firstRow:"+firstRow+",rowCount:"+rowCount+",paneOffset:"+paneOffset); } /** * Updates the location and the visibility of the focus indicator. */ qx.Proto._updateFocusIndicator = function() { if (this._focusedCol == null) { this._focusIndicator.hide(); } else { var xPos = this.getTablePaneModel().getX(this._focusedCol); if (xPos == -1) { this._focusIndicator.hide(); } else { var columnModel = this.getTable().getTableColumnModel(); var paneModel = this.getTablePaneModel(); var firstRow = this._tablePane.getFirstVisibleRow(); var rowHeight = this.getTable().getRowHeight(); this._focusIndicator.setHeight(rowHeight + 3); this._focusIndicator.setWidth(columnModel.getColumnWidth(this._focusedCol) + 3); this._focusIndicator.setTop((this._focusedRow - firstRow) * rowHeight - 2); this._focusIndicator.setLeft(paneModel.getColumnLeft(this._focusedCol) - 2); this._focusIndicator.show(); } } } // overridden qx.Proto.dispose = function() { if (this.getDisposed()) { return true; } if (this.getElement() != null) { this.getElement().onselectstart = null; } this._verScrollBar.dispose(); this._horScrollBar.dispose(); this._header.dispose(); this._headerClipper.dispose(); this._spacer.dispose(); this._top.dispose(); this._tablePane.dispose(); this._paneClipper.dispose(); if (this._resizeLine != null) { this._resizeLine.dispose(); } this.removeEventListener("mousemove", this._onmousemove, this); this.removeEventListener("mousedown", this._onmousedown, this); this.removeEventListener("mouseup", this._onmouseup, this); this.removeEventListener("click", this._onclick, this); this.removeEventListener("dblclick", this._ondblclick, this); this.removeEventListener("mouseout", this._onmouseout, this); var tablePaneModel = this.getTablePaneModel(); if (tablePaneModel != null) { tablePaneModel.removeEventListener("modelChanged", this._onPaneModelChanged, this); } return qx.ui.layout.VerticalBoxLayout.prototype.dispose.call(this); } /** {int} The minimum width a colum could get in pixels. */ qx.Class.MIN_COLUMN_WIDTH = 10; /** {int} The radius of the resize region in pixels. */ qx.Class.RESIZE_REGION_RADIUS = 5; /** * (int) The number of pixels the mouse may move between mouse down and mouse up * in order to count as a click. */ qx.Class.CLICK_TOLERANCE = 5; /** * (int) The mask for the horizontal scroll bar. * May be combined with {@link #VERTICAL_SCROLLBAR}. * * @see #getNeededScrollBars */ qx.Class.HORIZONTAL_SCROLLBAR = 1; /** * (int) The mask for the vertical scroll bar. * May be combined with {@link #HORIZONTAL_SCROLLBAR}. * * @see #getNeededScrollBars */ qx.Class.VERTICAL_SCROLLBAR = 2; /** * (string) The correct value for the CSS style attribute "cursor" for the * horizontal resize cursor. */ qx.Class.CURSOR_RESIZE_HORIZONTAL = (qx.sys.Client.getInstance().isGecko() && (qx.sys.Client.getInstance().getMajor() > 1 || qx.sys.Client.getInstance().getMinor() >= 8)) ? "ew-resize" : "e-resize";