UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,683 lines (1,439 loc) 71.9 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(table) { super(); 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 }, /* ***************************************************************************** EVENTS ***************************************************************************** */ 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 }, /** * Whether to reset the selection when the unpopulated table area is tapped. * The default is false which keeps the behaviour as before */ resetSelectionOnTapBelowRows: { check: "Boolean", init: false }, /** * 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" }, /** * If set then defines the minimum height of the focus indicator when editing */ minCellEditHeight: { check: "Integer", init: null, nullable: true } }, /* ***************************************************************************** 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() { 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(width) { if (this.isVerticalScrollBarVisible()) { width += this.getPaneInsetRight(); } this.setWidth(width); }, // overridden _createChildControlImpl(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 || super._createChildControlImpl(id); }, // property modifier _applyHorizontalScrollBarVisible(value, old) { if (value === null) { this.__horScrollBar.setVisibility("hidden"); } else { this.__horScrollBar.setVisibility(value ? "visible" : "excluded"); } }, // property modifier _applyVerticalScrollBarVisible(value, old) { this.__verScrollBar.setVisibility(value ? "visible" : "excluded"); }, // property modifier _applyTablePaneModel(value, old) { if (old != null) { old.removeListener("modelChanged", this._onPaneModelChanged, this); } value.addListener("modelChanged", this._onPaneModelChanged, this); }, // property modifier _applyShowCellFocusIndicator(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() { 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(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() { return this.__horScrollBar.getPosition(); }, /** * Set the current position of the vertical scroll bar. * * @param scrollX {Integer} The new scroll position. */ setScrollX(scrollX) { this.__horScrollBar.scrollTo(scrollX); }, /** * Returns the table this scroller belongs to. * * @return {qx.ui.table.Table} the table. */ getTable() { return this.__table; }, /** * Creates and returns an instance of pane clipper. * * @return {qx.ui.table.pane.Clipper} pane clipper. */ _createPaneClipper() { return new qx.ui.table.pane.Clipper(); }, /** * Creates and returns an instance of header clipper. * * @return {qx.ui.table.pane.Clipper} pane clipper. */ _createHeaderClipper() { return new qx.ui.table.pane.Clipper(); }, /** * Event handler. Called when the visibility of a column has changed. */ onColVisibilityChanged() { 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(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() { 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(firstRow, lastRow, firstColumn, lastColumn) { this.__tablePane.onTableModelDataChanged( firstRow, lastRow, firstColumn, lastColumn ); var rowCount = this.getTable().getTableModel().getRowCount(); if (rowCount != this.__lastRowCount) { this.updateVerScrollBarMaximum(); const focusedRow = this.getFocusedRow(); if (focusedRow !== null && focusedRow >= 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() { this.__tablePane.onSelectionChanged(); }, /** * Event handler. Called when the table gets or looses the focus. */ onFocusChanged() { this.__tablePane.onFocusChanged(); }, /** * Event handler. Called when the table model meta data has changed. * */ onTableModelMetaDataChanged() { this.__header.onTableModelMetaDataChanged(); this.__tablePane.onTableModelMetaDataChanged(); }, /** * Event handler. Called when the pane model has changed. */ _onPaneModelChanged() { this.__header.onPaneModelChanged(); this.__tablePane.onPaneModelChanged(); }, /** * Event listener for the pane clipper's resize event */ _onResizePane() { 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() { 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() { 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() { this.updateVerScrollBarMaximum(); this._updateContent(); }, /** * Event handler for the scroller's appear event */ _onAppear() { // after the Scroller appears we start the interval again this._startInterval(this.getScrollTimeout()); }, /** * Event handler for the disappear event */ _onDisappear() { // 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(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. */ __inOnScrollY: false, _onScrollY(e) { if (this.__inOnScrollY) { return; } var scrollbar = this.__verScrollBar; this.__inOnScrollY = true; // calculate delta so that one row is scrolled at a minimum var rowHeight = this.getTable().getRowHeight(); var delta = e.getData() - e.getOldData(); if (Math.abs(delta) > 1 && Math.abs(delta) < rowHeight) { delta = delta < 0 ? e.getOldData() - rowHeight : e.getOldData() + rowHeight; if ( delta >= 0 && delta <= scrollbar.getMaximum() && Math.abs(scrollbar.getPosition() - delta) > rowHeight ) { scrollbar.setPosition(delta); } } this.__inOnScrollY = false; this.fireDataEvent( "changeScrollY", scrollbar.getPosition(), e.getOldData() ); this._postponedUpdateContent(); }, /** * Event handler. Called when the user moved the mouse wheel. * * @param e {qx.event.type.Roll} the event. */ _onRoll(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(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(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 { 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(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(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(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 useResizeCursor = false; var resizeCol = this._getResizeColumnForPageX(pageX); if (resizeCol != -1) { // The pointer is over a resize region -> Show the right cursor useResizeCursor = true; } var cursor = useResizeCursor ? "col-resize" : null; this.getApplicationRoot().setGlobalCursor(cursor); this.setCursor(cursor); 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(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(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(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(e) { var table = this.getTable(); if (!table.getEnabled()) { return; } if (table.isEditing()) { table.stopEditing(); } 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(); return; } 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(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(e) { if (this.__resizeColumn != null) { this._stopResizeHeader(); } if (this._moveColumn != null) { this._stopMoveHeader(); } }, /** * Stop a resize session of the header. * */ _stopResizeHeader() { 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() { 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(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(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(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; } } else { if (row == null && this.getResetSelectionOnTapBelowRows()) { table.getSelectionModel().resetSelection(); } } }, /** * Event handler. Called when a context menu is invoked in a cell. * * @param e {qx.event.type.Pointer} the event. */ _onContextMenu(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(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(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(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(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() { 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(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() { 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(pageX, pageY) { var row = this._getRowForPagePos(pageX, pageY); if (row != -1 && row != null) { // The po