UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,761 lines (1,438 loc) 69.3 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2006 STZ-IDA, Germany, http://www.stz-ida.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Til Schneider (til132) * Jonathan Weiß (jonathan_rass) ************************************************************************ */ /** * Shows a whole meta column. This includes a {@link Header}, * a {@link Pane} and the needed scroll bars. This class handles the * virtual scrolling and does all the pointer event handling. * * @childControl header {qx.ui.table.pane.Header} header pane * @childControl pane {qx.ui.table.pane.Pane} table pane to show the data * @childControl focus-indicator {qx.ui.table.pane.FocusIndicator} shows the current focused cell * @childControl resize-line {qx.ui.core.Widget} resize line widget * @childControl scrollbar-x {qx.ui.core.scroll.ScrollBar?qx.ui.core.scroll.NativeScrollBar} * horizontal scrollbar widget (depends on the "qx.nativeScrollBars" setting which implementation is used) * @childControl scrollbar-y {qx.ui.core.scroll.ScrollBar?qx.ui.core.scroll.NativeScrollBar} * vertical scrollbar widget (depends on the "qx.nativeScrollBars" setting which implementation is used) */ qx.Class.define("qx.ui.table.pane.Scroller", { extend : qx.ui.core.Widget, include : [qx.ui.core.scroll.MScrollBarFactory], /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * @param table {qx.ui.table.Table} the table the scroller belongs to. */ construct : function(table) { this.base(arguments); this.__table = table; // init layout var grid = new qx.ui.layout.Grid(); grid.setColumnFlex(0, 1); grid.setRowFlex(1, 1); this._setLayout(grid); // init child controls this.__header = this._showChildControl("header"); this.__tablePane = this._showChildControl("pane"); // the top line containing the header clipper and the top right widget this.__top = new qx.ui.container.Composite(new qx.ui.layout.HBox()).set({ minWidth: 0 }); this._add(this.__top, {row: 0, column: 0, colSpan: 2}); // embed header into a scrollable container this._headerClipper = this._createHeaderClipper(); this._headerClipper.add(this.__header); this._headerClipper.addListener("losecapture", this._onChangeCaptureHeader, this); this._headerClipper.addListener("pointermove", this._onPointermoveHeader, this); this._headerClipper.addListener("pointerdown", this._onPointerdownHeader, this); this._headerClipper.addListener("pointerup", this._onPointerupHeader, this); this._headerClipper.addListener("tap", this._onTapHeader, this); this.__top.add(this._headerClipper, {flex: 1}); // embed pane into a scrollable container this._paneClipper = this._createPaneClipper(); this._paneClipper.add(this.__tablePane); this._paneClipper.addListener("roll", this._onRoll, this); this._paneClipper.addListener("pointermove", this._onPointermovePane, this); this._paneClipper.addListener("pointerdown", this._onPointerdownPane, this); this._paneClipper.addListener("tap", this._onTapPane, this); this._paneClipper.addListener("contextmenu", this._onTapPane, this); this._paneClipper.addListener("contextmenu", this._onContextMenu, this); if (qx.core.Environment.get("device.type") === "desktop") { this._paneClipper.addListener("dblclick", this._onDbltapPane, this); } else { this._paneClipper.addListener("dbltap", this._onDbltapPane, this); } this._paneClipper.addListener("resize", this._onResizePane, this); // if we have overlayed scroll bars, we should use a separate container if (qx.core.Environment.get("os.scrollBarOverlayed")) { this.__clipperContainer = new qx.ui.container.Composite(); this.__clipperContainer.setLayout(new qx.ui.layout.Canvas()); this.__clipperContainer.add(this._paneClipper, {edge: 0}); this._add(this.__clipperContainer, {row: 1, column: 0}); } else { this._add(this._paneClipper, {row: 1, column: 0}); } // init scroll bars this.__horScrollBar = this._showChildControl("scrollbar-x"); this.__verScrollBar = this._showChildControl("scrollbar-y"); // init focus indicator this.__focusIndicator = this.getChildControl("focus-indicator"); // need to run the apply method at least once [BUG #4057] this.initShowCellFocusIndicator(); // force creation of the resize line this.getChildControl("resize-line").hide(); this.addListener("pointerout", this._onPointerout, this); this.addListener("appear", this._onAppear, this); this.addListener("disappear", this._onDisappear, this); this.__timer = new qx.event.Timer(); this.__timer.addListener("interval", this._oninterval, this); this.initScrollTimeout(); }, /* ***************************************************************************** STATICS ***************************************************************************** */ statics : { /** @type {int} The minimum width a column could get in pixels. */ MIN_COLUMN_WIDTH : 10, /** @type {int} The radius of the resize region in pixels. */ RESIZE_REGION_RADIUS : 5, /** * (int) The number of pixels the pointer may move between pointer down and pointer up * in order to count as a tap. */ TAP_TOLERANCE : 5, /** * (int) The mask for the horizontal scroll bar. * May be combined with {@link #VERTICAL_SCROLLBAR}. * * @see #getNeededScrollBars */ HORIZONTAL_SCROLLBAR : 1, /** * (int) The mask for the vertical scroll bar. * May be combined with {@link #HORIZONTAL_SCROLLBAR}. * * @see #getNeededScrollBars */ VERTICAL_SCROLLBAR : 2 }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ events : { /** Dispatched if the pane is scrolled horizontally */ "changeScrollY" : "qx.event.type.Data", /** Dispatched if the pane is scrolled vertically */ "changeScrollX" : "qx.event.type.Data", /**See {@link qx.ui.table.Table#cellTap}.*/ "cellTap" : "qx.ui.table.pane.CellEvent", /*** See {@link qx.ui.table.Table#cellDbltap}.*/ "cellDbltap" : "qx.ui.table.pane.CellEvent", /**See {@link qx.ui.table.Table#cellContextmenu}.*/ "cellContextmenu" : "qx.ui.table.pane.CellEvent", /** Dispatched when a sortable header was tapped */ "beforeSort" : "qx.event.type.Data" }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties : { /** * Whether to show the horizontal scroll bar. This is a tri-state * value. `true` means show the scroll bar; `false` means exclude it; null * means hide it so it retains its space but doesn't show a scroll bar. */ horizontalScrollBarVisible : { check : "Boolean", init : false, apply : "_applyHorizontalScrollBarVisible", event : "changeHorizontalScrollBarVisible", nullable : true }, /** Whether to show the vertical scroll bar */ verticalScrollBarVisible : { check : "Boolean", init : false, apply : "_applyVerticalScrollBarVisible", event : "changeVerticalScrollBarVisible" }, /** The table pane model. */ tablePaneModel : { check : "qx.ui.table.pane.Model", apply : "_applyTablePaneModel", event : "changeTablePaneModel" }, /** * 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 pointer button. */ liveResize : { check : "Boolean", init : false }, /** * Whether the focus should moved when the pointer is moved over a cell. If false * the focus is only moved on pointer taps. */ focusCellOnPointerMove : { check : "Boolean", init : false }, /** * Whether to handle selections via the selection manager before setting the * focus. The traditional behavior is to handle selections after setting the * focus, but setting the focus means redrawing portions of the table, and * some subclasses may want to modify the data to be displayed based on the * selection. */ selectBeforeFocus : { check : "Boolean", init : false }, /** * Whether the cell focus indicator should be shown */ showCellFocusIndicator : { check : "Boolean", init : true, apply : "_applyShowCellFocusIndicator" }, /** * By default, the "cellContextmenu" event is fired only when a data cell * is right-clicked. It is not fired when a right-click occurs in the * empty area of the table below the last data row. By turning on this * property, "cellContextMenu" events will also be generated when a * right-click occurs in that empty area. In such a case, row identifier * in the event data will be null, so event handlers can check (row === * null) to handle this case. */ contextMenuFromDataCellsOnly : { check : "Boolean", init : true }, /** * Whether to reset the selection when a header cell is tapped. Since * most data models do not have provisions to retain a selection after * sorting, the default is to reset the selection in this case. Some data * models, however, do have the capability to retain the selection, so * when using those, this property should be set to false. */ resetSelectionOnHeaderTap : { check : "Boolean", init : true }, /** * Interval time (in milliseconds) for the table update timer. * Setting this to 0 clears the timer. */ scrollTimeout : { check : "Integer", init : 100, apply : "_applyScrollTimeout" }, appearance : { refine : true, init : "table-scroller" } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { __lastRowCount : null, __table : null, __updateInterval : null, __updateContentPlanned : null, __onintervalWrapper : null, _moveColumn : null, __lastMoveColPos : null, _lastMoveTargetX : null, _lastMoveTargetScroller : null, __lastMovePointerPageX : null, __resizeColumn : null, __lastResizePointerPageX : null, __lastResizeWidth : null, __lastPointerDownCell : null, __firedTapEvent : false, __ignoreTap : null, __lastPointerPageX : null, __lastPointerPageY : null, __focusedCol : null, __focusedRow : null, _cellEditor : null, __cellEditorFactory : null, __topRightWidget : null, __horScrollBar : null, __verScrollBar : null, __header : null, _headerClipper : null, __tablePane : null, _paneClipper : null, __clipperContainer : null, __focusIndicator : null, __top : null, __timer : null, __focusIndicatorPointerDownListener: null, /** * The right inset of the pane. The right inset is the maximum of the * top right widget width and the scrollbar width (if visible). * * @return {Integer} The right inset of the pane */ getPaneInsetRight : function() { var topRight = this.getTopRightWidget(); var topRightWidth = topRight && topRight.isVisible() && topRight.getBounds() ? topRight.getBounds().width + topRight.getMarginLeft() + topRight.getMarginRight() : 0; var scrollBar = this.__verScrollBar; var scrollBarWidth = this.getVerticalScrollBarVisible() ? this.getVerticalScrollBarWidth() + scrollBar.getMarginLeft() + scrollBar.getMarginRight() : 0; return Math.max(topRightWidth, scrollBarWidth); }, /** * Set the pane's width * * @param width {Integer} The pane's width */ setPaneWidth : function(width) { if (this.isVerticalScrollBarVisible()) { width += this.getPaneInsetRight(); } this.setWidth(width); }, // overridden _createChildControlImpl : function(id, hash) { var control; switch(id) { case "header": control = (this.getTable().getNewTablePaneHeader())(this); break; case "pane": control = (this.getTable().getNewTablePane())(this); break; case "focus-indicator": control = new qx.ui.table.pane.FocusIndicator(this); control.setUserBounds(0, 0, 0, 0); control.setZIndex(1000); control.addListener("pointerup", this._onPointerupFocusIndicator, this); this._paneClipper.add(control); control.show(); // must be active for editor to operate control.setDecorator(null); // it can be initially invisible, though. break; case "resize-line": control = new qx.ui.core.Widget(); control.setUserBounds(0, 0, 0, 0); control.setZIndex(1000); this._paneClipper.add(control); break; case "scrollbar-x": control = this._createScrollBar("horizontal").set({ alignY: "bottom" }); control.addListener("scroll", this._onScrollX, this); if (this.__clipperContainer != null) { control.setMinHeight(qx.ui.core.scroll.AbstractScrollArea.DEFAULT_SCROLLBAR_WIDTH); this.__clipperContainer.add(control, {bottom: 0, right: 0, left: 0}); } else { this._add(control, {row: 2, column: 0}); } break; case "scrollbar-y": control = this._createScrollBar("vertical"); control.addListener("scroll", this._onScrollY, this); if (this.__clipperContainer != null) { this.__clipperContainer.add(control, {right: 0, bottom: 0, top: 0}); } else { this._add(control, {row: 1, column: 1}); } break; } return control || this.base(arguments, id); }, // property modifier _applyHorizontalScrollBarVisible : function(value, old) { if (value === null) { this.__horScrollBar.setVisibility("hidden"); } else { this.__horScrollBar.setVisibility(value ? "visible" : "excluded"); } }, // property modifier _applyVerticalScrollBarVisible : function(value, old) { this.__verScrollBar.setVisibility(value ? "visible" : "excluded"); }, // property modifier _applyTablePaneModel : function(value, old) { if (old != null) { old.removeListener("modelChanged", this._onPaneModelChanged, this); } value.addListener("modelChanged", this._onPaneModelChanged, this); }, // property modifier _applyShowCellFocusIndicator : function(value, old) { if(value) { this.__focusIndicator.setDecorator("table-scroller-focus-indicator"); this._updateFocusIndicator(); } else { if(this.__focusIndicator) { this.__focusIndicator.setDecorator(null); } } }, /** * Get the current position of the vertical scroll bar. * * @return {Integer} The current scroll position. */ getScrollY : function() { return this.__verScrollBar.getPosition(); }, /** * Set the current position of the vertical scroll bar. * * @param scrollY {Integer} The new scroll position. * @param renderSync {Boolean?false} Whether the table update should be * performed synchronously. */ setScrollY : function(scrollY, renderSync) { this.__verScrollBar.scrollTo(scrollY); if (renderSync) { this._updateContent(); } }, /** * Get the current position of the vertical scroll bar. * * @return {Integer} The current scroll position. */ getScrollX : function() { return this.__horScrollBar.getPosition(); }, /** * Set the current position of the vertical scroll bar. * * @param scrollX {Integer} The new scroll position. */ setScrollX : function(scrollX) { this.__horScrollBar.scrollTo(scrollX); }, /** * Returns the table this scroller belongs to. * * @return {qx.ui.table.Table} the table. */ getTable : function() { return this.__table; }, /** * Creates and returns an instance of pane clipper. * * @return {qx.ui.table.pane.Clipper} pane clipper. */ _createPaneClipper : function() { return new qx.ui.table.pane.Clipper(); }, /** * Creates and returns an instance of header clipper. * * @return {qx.ui.table.pane.Clipper} pane clipper. */ _createHeaderClipper : function() { return new qx.ui.table.pane.Clipper(); }, /** * Event handler. Called when the visibility of a column has changed. */ onColVisibilityChanged : function() { this.updateHorScrollBarMaximum(); this._updateFocusIndicator(); }, /** * Sets the column width. * * @param col {Integer} the column to change the width for. * @param width {Integer} the new width. */ setColumnWidth : function(col, width) { this.__header.setColumnWidth(col, width); this.__tablePane.setColumnWidth(col, width); var paneModel = this.getTablePaneModel(); var x = paneModel.getX(col); if (x != -1) { // The change was in this scroller this.updateHorScrollBarMaximum(); this._updateFocusIndicator(); } }, /** * Event handler. Called when the column order has changed. * */ onColOrderChanged : function() { this.__header.onColOrderChanged(); this.__tablePane.onColOrderChanged(); this.updateHorScrollBarMaximum(); }, /** * Event handler. Called when the table model has changed. * * @param firstRow {Integer} The index of the first row that has changed. * @param lastRow {Integer} The index of the last row that has changed. * @param firstColumn {Integer} The model index of the first column that has changed. * @param lastColumn {Integer} The model index of the last column that has changed. */ onTableModelDataChanged : function(firstRow, lastRow, firstColumn, lastColumn) { this.__tablePane.onTableModelDataChanged(firstRow, lastRow, firstColumn, lastColumn); var rowCount = this.getTable().getTableModel().getRowCount(); if (rowCount != this.__lastRowCount) { this.updateVerScrollBarMaximum(); if (this.getFocusedRow() >= rowCount) { if (rowCount == 0) { this.setFocusedCell(null, null); } else { this.setFocusedCell(this.getFocusedColumn(), rowCount - 1); } } this.__lastRowCount = rowCount; } }, /** * Event handler. Called when the selection has changed. */ onSelectionChanged : function() { this.__tablePane.onSelectionChanged(); }, /** * Event handler. Called when the table gets or looses the focus. */ onFocusChanged : function() { this.__tablePane.onFocusChanged(); }, /** * Event handler. Called when the table model meta data has changed. * */ onTableModelMetaDataChanged : function() { this.__header.onTableModelMetaDataChanged(); this.__tablePane.onTableModelMetaDataChanged(); }, /** * Event handler. Called when the pane model has changed. */ _onPaneModelChanged : function() { this.__header.onPaneModelChanged(); this.__tablePane.onPaneModelChanged(); }, /** * Event listener for the pane clipper's resize event */ _onResizePane : function() { this.updateHorScrollBarMaximum(); this.updateVerScrollBarMaximum(); // The height has changed -> Update content this._updateContent(); this.__header._updateContent(); this.__table._updateScrollBarVisibility(); }, /** * Updates the maximum of the horizontal scroll bar, so it corresponds to the * total width of the columns in the table pane. */ updateHorScrollBarMaximum : function() { var paneSize = this._paneClipper.getInnerSize(); if (!paneSize) { // will be called on the next resize event again return; } var scrollSize = this.getTablePaneModel().getTotalWidth(); var scrollBar = this.__horScrollBar; if (paneSize.width < scrollSize) { var max = Math.max(0, scrollSize - paneSize.width); scrollBar.setMaximum(max); scrollBar.setKnobFactor(paneSize.width / scrollSize); var pos = scrollBar.getPosition(); scrollBar.setPosition(Math.min(pos, max)); } else { scrollBar.setMaximum(0); scrollBar.setKnobFactor(1); scrollBar.setPosition(0); } }, /** * Updates the maximum of the vertical scroll bar, so it corresponds to the * number of rows in the table. */ updateVerScrollBarMaximum : function() { var paneSize = this._paneClipper.getInnerSize(); if (!paneSize) { // will be called on the next resize event again return; } var tableModel = this.getTable().getTableModel(); var rowCount = tableModel.getRowCount(); if (this.getTable().getKeepFirstVisibleRowComplete()) { rowCount += 1; } var rowHeight = this.getTable().getRowHeight(); var scrollSize = rowCount * rowHeight; var scrollBar = this.__verScrollBar; if (paneSize.height < scrollSize) { var max = Math.max(0, scrollSize - paneSize.height); scrollBar.setMaximum(max); scrollBar.setKnobFactor(paneSize.height / scrollSize); var pos = scrollBar.getPosition(); scrollBar.setPosition(Math.min(pos, max)); } else { scrollBar.setMaximum(0); scrollBar.setKnobFactor(1); scrollBar.setPosition(0); } }, /** * Event handler. Called when the table property "keepFirstVisibleRowComplete" * changed. */ onKeepFirstVisibleRowCompleteChanged : function() { this.updateVerScrollBarMaximum(); this._updateContent(); }, /** * Event handler for the scroller's appear event */ _onAppear : function() { // after the Scroller appears we start the interval again this._startInterval(this.getScrollTimeout()); }, /** * Event handler for the disappear event */ _onDisappear : function() { // before the scroller disappears we need to stop it this._stopInterval(); }, /** * Event handler. Called when the horizontal scroll bar moved. * * @param e {Map} the event. */ _onScrollX : function(e) { var scrollLeft = e.getData(); this.fireDataEvent("changeScrollX", scrollLeft, e.getOldData()); this._headerClipper.scrollToX(scrollLeft); this._paneClipper.scrollToX(scrollLeft); }, /** * Event handler. Called when the vertical scroll bar moved. * * @param e {Map} the event. */ _onScrollY : function(e) { this.fireDataEvent("changeScrollY", e.getData(), e.getOldData()); this._postponedUpdateContent(); }, /** * Event handler. Called when the user moved the mouse wheel. * * @param e {qx.event.type.Roll} the event. */ _onRoll : function(e) { var table = this.getTable(); if (e.getPointerType() == "mouse" || !table.getEnabled()) { return; } // vertical scrolling var delta = e.getDelta(); // normalize that at least one step is scrolled at a time if (delta.y > 0 && delta.y < 1) { delta.y = 1; } else if (delta.y < 0 && delta.y > -1) { delta.y = -1; } this.__verScrollBar.scrollBy(parseInt(delta.y, 10)); var scrolled = delta.y != 0 && !this.__isAtEdge(this.__verScrollBar, delta.y); // horizontal scrolling // normalize that at least one step is scrolled at a time if (delta.x > 0 && delta.x < 1) { delta.x = 1; } else if (delta.x < 0 && delta.x > -1) { delta.x = -1; } this.__horScrollBar.scrollBy(parseInt(delta.x, 10)); // Update the focus if (this.__lastPointerPageX && this.getFocusCellOnPointerMove()) { this._focusCellAtPagePos(this.__lastPointerPageX, this.__lastPointerPageY); } scrolled = scrolled || (delta.x != 0 && !this.__isAtEdge(this.__horScrollBar, delta.x)); // pass the event to the parent if the scrollbar is at an edge if (scrolled) { e.stop(); } else { e.stopMomentum(); } }, /** * Checks if the table has been scrolled. * @param scrollBar {qx.ui.core.scroll.IScrollBar} The scrollbar to check * @param delta {Number} The scroll delta. * @return {Boolean} <code>true</code>, if the scrolling is a the edge */ __isAtEdge : function(scrollBar, delta) { var position = scrollBar.getPosition(); return (delta < 0 && position <= 0) || (delta > 0 && position >= scrollBar.getMaximum()); }, /** * Common column resize logic. * * @param pageX {Integer} the current pointer x position. */ __handleResizeColumn : function(pageX) { var table = this.getTable(); // We are currently resizing -> Update the position var headerCell = this.__header.getHeaderWidgetAtColumn(this.__resizeColumn); var minColumnWidth = headerCell.getSizeHint().minWidth; var newWidth = Math.max(minColumnWidth, this.__lastResizeWidth + pageX - this.__lastResizePointerPageX); if (this.getLiveResize()) { var columnModel = table.getTableColumnModel(); columnModel.setColumnWidth(this.__resizeColumn, newWidth, true); } else { this.__header.setColumnWidth(this.__resizeColumn, newWidth, true); var paneModel = this.getTablePaneModel(); this._showResizeLine(paneModel.getColumnLeft(this.__resizeColumn) + newWidth); } this.__lastResizePointerPageX += newWidth - this.__lastResizeWidth; this.__lastResizeWidth = newWidth; }, /** * Common column move logic. * * @param pageX {Integer} the current pointer x position. * */ __handleMoveColumn : function(pageX) { // We are moving a column // Check whether we moved outside the tap tolerance so we can start // showing the column move feedback // (showing the column move feedback prevents the ontap event) var tapTolerance = qx.ui.table.pane.Scroller.TAP_TOLERANCE; if (this.__header.isShowingColumnMoveFeedback() || pageX > this.__lastMovePointerPageX + tapTolerance || pageX < this.__lastMovePointerPageX - tapTolerance) { this.__lastMoveColPos += pageX - this.__lastMovePointerPageX; 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.__lastMovePointerPageX = pageX; } }, /** * Event handler. Called when the user moved the pointer over the header. * * @param e {Map} the event. */ _onPointermoveHeader : function(e) { var table = this.getTable(); if (! table.getEnabled()) { return; } var useResizeCursor = false; var pointerOverColumn = null; var pageX = e.getDocumentLeft(); var pageY = e.getDocumentTop(); // Workaround: In onmousewheel the event has wrong coordinates for pageX // and pageY. So we remember the last move event. this.__lastPointerPageX = pageX; this.__lastPointerPageY = pageY; if (this.__resizeColumn != null) { // We are currently resizing -> Update the position this.__handleResizeColumn(pageX); useResizeCursor = true; e.stopPropagation(); } else if (this._moveColumn != null) { // We are moving a column this.__handleMoveColumn(pageX); e.stopPropagation(); } else { var resizeCol = this._getResizeColumnForPageX(pageX); if (resizeCol != -1) { // The pointer is over a resize region -> Show the right cursor useResizeCursor = true; } else { var tableModel = table.getTableModel(); var col = this._getColumnForPageX(pageX); if (col != null && tableModel.isColumnSortable(col)) { pointerOverColumn = col; } } } var cursor = useResizeCursor ? "col-resize" : null; this.getApplicationRoot().setGlobalCursor(cursor); this.setCursor(cursor); this.__header.setPointerOverColumn(pointerOverColumn); }, /** * Event handler. Called when the user moved the pointer over the pane. * * @param e {Map} the event. */ _onPointermovePane : function(e) { var table = this.getTable(); if (! table.getEnabled()) { return; } //var useResizeCursor = false; var pageX = e.getDocumentLeft(); var pageY = e.getDocumentTop(); // Workaround: In onpointerwheel the event has wrong coordinates for pageX // and pageY. So we remember the last move event. this.__lastPointerPageX = pageX; this.__lastPointerPageY = pageY; var row = this._getRowForPagePos(pageX, pageY); if (row != null && this._getColumnForPageX(pageX) != null) { // The pointer is over the data -> update the focus if (this.getFocusCellOnPointerMove()) { this._focusCellAtPagePos(pageX, pageY); } } this.__header.setPointerOverColumn(null); }, /** * Event handler. Called when the user pressed a pointer button over the header. * * @param e {Map} the event. */ _onPointerdownHeader : function(e) { if (! this.getTable().getEnabled()) { return; } var pageX = e.getDocumentLeft(); // pointer is in header var resizeCol = this._getResizeColumnForPageX(pageX); if (resizeCol != -1) { // The pointer is over a resize region -> Start resizing this._startResizeHeader(resizeCol, pageX); e.stop(); } else { // The pointer is not in a resize region var moveCol = this._getColumnForPageX(pageX); if (moveCol != null) { this._startMoveHeader(moveCol, pageX); e.stop(); } } }, /** * Start a resize session of the header. * * @param resizeCol {Integer} the column index * @param pageX {Integer} x coordinate of the pointer event */ _startResizeHeader : function(resizeCol, pageX) { var columnModel = this.getTable().getTableColumnModel(); // The pointer is over a resize region -> Start resizing this.__resizeColumn = resizeCol; this.__lastResizePointerPageX = pageX; this.__lastResizeWidth = columnModel.getColumnWidth(this.__resizeColumn); this._headerClipper.capture(); }, /** * Start a move session of the header. * * @param moveCol {Integer} the column index * @param pageX {Integer} x coordinate of the pointer event */ _startMoveHeader : function(moveCol, pageX) { // Prepare column moving this._moveColumn = moveCol; this.__lastMovePointerPageX = pageX; this.__lastMoveColPos = this.getTablePaneModel().getColumnLeft(moveCol); this._headerClipper.capture(); }, /** * Event handler. Called when the user pressed a pointer button over the pane. * * @param e {Map} the event. */ _onPointerdownPane : function(e) { var table = this.getTable(); if (!table.getEnabled()) { return; } if (table.isEditing()) { table.stopEditing(); } var pageX = e.getDocumentLeft(); var pageY = e.getDocumentTop(); var row = this._getRowForPagePos(pageX, pageY); var col = this._getColumnForPageX(pageX); if (row !== null) { // The focus indicator blocks the tap event on the scroller so we // store the current cell and listen for the pointerup event on the // focus indicator // // INVARIANT: // The members of this object always contain the last position of // the cell on which the pointerdown event occurred. // *** These values are never cleared! ***. // Different browsers/OS combinations issue events in different // orders, and the context menu event, in particular, can be issued // early or late (Firefox on Linux issues it early; Firefox on // Windows issues it late) so no one may clear these values. // this.__lastPointerDownCell = { row : row, col : col }; // On the other hand, we need to know if we've issued the tap event // so we don't issue it twice, both from pointer-up on the focus // indicator, and from the tap even on the pane. Both possibilities // are necessary, however, to maintain the qooxdoo order of events. this.__firedTapEvent = false; } }, /** * Event handler for the focus indicator's pointerup event * * @param e {qx.event.type.Pointer} The pointer event */ _onPointerupFocusIndicator : function(e) { if (this.__lastPointerDownCell && !this.__firedTapEvent && !this.isEditing() && this.__focusIndicator.getRow() == this.__lastPointerDownCell.row && this.__focusIndicator.getColumn() == this.__lastPointerDownCell.col) { this.fireEvent("cellTap", qx.ui.table.pane.CellEvent, [ this, e, this.__lastPointerDownCell.row, this.__lastPointerDownCell.col ], true); this.__firedTapEvent = true; } else if (!this.isEditing()) { // if no cellTap event should be fired, act like a pointerdown which // invokes the change of the selection e.g. [BUG #1632] this._onPointerdownPane(e); } }, /** * Event handler. Called when the event capturing of the header changed. * Stops/finishes an active header resize/move session if it lost capturing * during the session to stay in a stable state. * * @param e {qx.event.type.Data} The data event */ _onChangeCaptureHeader : function(e) { if (this.__resizeColumn != null) { this._stopResizeHeader(); } if (this._moveColumn != null) { this._stopMoveHeader(); } }, /** * Stop a resize session of the header. * */ _stopResizeHeader : function() { var columnModel = this.getTable().getTableColumnModel(); // We are currently resizing -> Finish resizing if (! this.getLiveResize()) { this._hideResizeLine(); columnModel.setColumnWidth(this.__resizeColumn, this.__lastResizeWidth, true); } this.__resizeColumn = null; this._headerClipper.releaseCapture(); this.getApplicationRoot().setGlobalCursor(null); this.setCursor(null); }, /** * Stop a move session of the header. * */ _stopMoveHeader : function() { var columnModel = this.getTable().getTableColumnModel(); var paneModel = this.getTablePaneModel(); // 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); // update the focus indicator including the editor this._updateFocusIndicator(); } } this._moveColumn = null; this._lastMoveTargetX = null; this._headerClipper.releaseCapture(); }, /** * Event handler. Called when the user released a pointer button over the header. * * @param e {Map} the event. */ _onPointerupHeader : function(e) { var table = this.getTable(); if (! table.getEnabled()) { return; } if (this.__resizeColumn != null) { this._stopResizeHeader(); this.__ignoreTap = true; e.stop(); } else if (this._moveColumn != null) { this._stopMoveHeader(); e.stop(); } }, /** * Event handler. Called when the user tapped a pointer button over the header. * * @param e {Map} the event. */ _onTapHeader : function(e) { if (this.__ignoreTap) { this.__ignoreTap = false; return; } var table = this.getTable(); if (!table.getEnabled()) { return; } var tableModel = table.getTableModel(); var pageX = e.getDocumentLeft(); var resizeCol = this._getResizeColumnForPageX(pageX); if (resizeCol == -1) { // pointer 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(); var data = { column : col, ascending : ascending, tapEvent : e }; if (this.fireDataEvent("beforeSort", data, null, true)) { // Stop cell editing if (table.isEditing()) { table.stopEditing(); } tableModel.sortByColumn(col, ascending); if (this.getResetSelectionOnHeaderTap()) { table.getSelectionModel().resetSelection(); } } } } e.stop(); }, /** * Event handler. Called when the user tapped a pointer button over the pane. * * @param e {Map} the event. */ _onTapPane : function(e) { var table = this.getTable(); if (!table.getEnabled()) { return; } var pageX = e.getDocumentLeft(); var pageY = e.getDocumentTop(); var row = this._getRowForPagePos(pageX, pageY); var col = this._getColumnForPageX(pageX); if (row != null && col != null) { var selectBeforeFocus = this.getSelectBeforeFocus(); if (selectBeforeFocus) { table.getSelectionManager().handleTap(row, e); } // The pointer is over the data -> update the focus if (!this.getFocusCellOnPointerMove()) { this._focusCellAtPagePos(pageX, pageY); } if (!selectBeforeFocus) { table.getSelectionManager().handleTap(row, e); } if (this.__focusIndicator.isHidden() || (this.__lastPointerDownCell && !this.__firedTapEvent && !this.isEditing() && row == this.__lastPointerDownCell.row && col == this.__lastPointerDownCell.col)) { this.fireEvent("cellTap", qx.ui.table.pane.CellEvent, [this, e, row, col], true); this.__firedTapEvent = true; } } }, /** * Event handler. Called when a context menu is invoked in a cell. * * @param e {qx.event.type.Pointer} the event. */ _onContextMenu : function(e) { var pageX = e.getDocumentLeft(); var pageY = e.getDocumentTop(); var row = this._getRowForPagePos(pageX, pageY); var col = this._getColumnForPageX(pageX); /* * The 'row' value will be null if the right-click was in the blank * area below the last data row. Some applications desire to receive * the context menu event anyway, and can set the property value of * contextMenuFromDataCellsOnly to false to achieve that. */ if (row === null && this.getContextMenuFromDataCellsOnly()) { return; } if (! this.getShowCellFocusIndicator() || row === null || (this.__lastPointerDownCell && row == this.__lastPointerDownCell.row && col == this.__lastPointerDownCell.col)) { this.fireEvent("cellContextmenu", qx.ui.table.pane.CellEvent, [this, e, row, col], true); // Now that the cellContextmenu handler has had a chance to build // the menu for this cell, display it (if there is one). var menu = this.getTable().getContextMenu(); if (menu) { // A menu with no children means don't display any context menu // including the default context menu even if the default context // menu is allowed to be displayed normally. There's no need to // actually show an empty menu, though. if (menu.getChildren().length > 0) { menu.openAtPointer(e); } else { menu.exclude(); } // Do not show native menu e.preventDefault(); } } }, // overridden _onContextMenuOpen : function(e) { // This is Widget's context menu handler which typically retrieves // and displays the menu as soon as it receives a "contextmenu" event. // We want to allow the cellContextmenu handler to create the menu, // so we'll override this method with a null one, and do the menu // placement and display handling in our _onContextMenu method. }, /** * Event handler. Called when the user double tapped a pointer button over the pane. * * @param e {Map} the event. */ _onDbltapPane : function(e) { var pageX = e.getDocumentLeft(); var pageY = e.getDocumentTop(); var col = this._getColumnForPageX(pageX); if (col !== null) { this._focusCellAtPagePos(pageX, pageY); this.startEditing(); var row = this._getRowForPagePos(pageX, pageY); if (row != -1 && row != null) { this.fireEvent("cellDbltap", qx.ui.table.pane.CellEvent, [this, e, row], true); } } }, /** * Event handler. Called when the pointer moved out. * * @param e {Map} the event. */ _onPointerout : function(e) { var table = this.getTable(); if (!table.getEnabled()) { return; } // Reset the resize cursor when the pointer leaves the header // If currently a column is resized then do nothing // (the cursor will be reset on pointerup) if (this.__resizeColumn == null) { this.setCursor(null); this.getApplicationRoot().setGlobalCursor(null); } this.__header.setPointerOverColumn(null); // in case the focus follows the pointer, it should be remove on pointerout if (this.getFocusCellOnPointerMove()) { this.__table.setFocusedCell(); } }, /** * Shows the resize line. * * @param x {Integer} the position where to show the line (in pixels, relative to * the left side of the pane). */ _showResizeLine : function(x) { var resizeLine = this._showChildControl("resize-line"); var width = resizeLine.getWidth(); var paneBounds = this._paneClipper.getBounds(); resizeLine.setUserBounds( x - Math.round(width/2), 0, width, paneBounds.height ); }, /** * Hides the resize line. */ _hideResizeLine : function() { this._excludeChildControl("resize-line"); }, /** * Shows the feedback shown while a column is moved by the user. * * @param pageX {Integer} the x position of the pointer in the page (in pixels). * @return {Integer} the visible x position of the column in the whole table. */ showColumnMoveFeedback : function(pageX) { var paneModel = this.getTablePaneModel(); var columnModel = this.getTable().getTableColumnModel(); var paneLeft = this.__tablePane.getContentLocation().left; var colCount = paneModel.getColumnCount(); var targetXPos = 0; var targetX = 0; var currX = paneLeft; 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 - paneLeft; } // Ensure targetX is visible var scrollerLeft = this._paneClipper.getContentLocation().left; var scrollerWidth = this._paneClipper.getBounds().width; var scrollX = scrollerLeft - paneLeft; // NOTE: +2/-1 because of feedback width targetX = qx.lang.Number.limit(targetX, scrollX + 2, scrollX + scrollerWidth - 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. */ hideColumnMoveFeedback : function() { this._hideResizeLine(); }, /** * Sets the focus to the cell that's located at the page position * <code>pageX</code>/<code>pageY</code>. If there is no cell at that position, * nothing happens. * * @param pageX {Integer} the x position in the page (in pixels). * @param pageY {Integer} the y position in the page (in pixels). */ _focusCellAtPagePos : function(pageX, pageY) { var row = this._getRowForPagePos(pageX, pageY); if (row != -1 && row != null) { // The pointer is over the data -> update the focus var col = this._getColumnForPageX(pageX); this.__table.setFocusedCell(col, row); } }, /** * Sets the currently focused cell. * * @param col {Integer} the model index of the focused cell's column. * @param row {Integer} the model index of the focused cell's row. */ setFocusedCell : function(col, row) { if (!this.isEditing()) { this.__tablePane.setFocusedCell(col, row, this.__updateContentPlanned); this.__focusedCol = col; this.__focusedRow = row; this._updateFocusIndicator(); } }, /** * Returns the column of currently focused cell. * * @return {Integer} the model index of the focused cell's column. */ getFocusedColumn : function() { return this.__focusedCol; }, /** * Returns the row of currently focused cell. * * @return {Integer} the model index of the focused cell's column. */ getFocusedRow : function() { return this.__focusedRow; }, /** * Scrolls a cell visible. * * @param col {Integer} the model index of the colu