@qooxdoo/framework
Version:
The JS Framework for Coders
1,683 lines (1,439 loc) • 71.9 kB
JavaScript
/* ************************************************************************
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