/* ************************************************************************ 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";