r25048: From the archives (patch found in one of my old working trees):
[jelmer/samba4-debian.git] / webapps / qooxdoo-0.6.5-sdk / frontend / framework / source / class / qx / ui / table / TablePaneScroller.js
1 /* ************************************************************************
2
3    qooxdoo - the new era of web development
4
5    http://qooxdoo.org
6
7    Copyright:
8      2006 STZ-IDA, Germany, http://www.stz-ida.de
9
10    License:
11      LGPL: http://www.gnu.org/licenses/lgpl.html
12      EPL: http://www.eclipse.org/org/documents/epl-v10.php
13      See the LICENSE file in the project's top-level directory for details.
14
15    Authors:
16      * Til Schneider (til132)
17
18 ************************************************************************ */
19
20 /* ************************************************************************
21
22 #module(ui_table)
23
24 ************************************************************************ */
25
26 /**
27  * Shows a whole meta column. This includes a {@link TablePaneHeader},
28  * a {@link TablePane} and the needed scroll bars. This class handles the
29  * virtual scrolling and does all the mouse event handling.
30  *
31  * @param table {Table} the table the scroller belongs to.
32  */
33 qx.OO.defineClass("qx.ui.table.TablePaneScroller", qx.ui.layout.VerticalBoxLayout,
34 function(table) {
35   qx.ui.layout.VerticalBoxLayout.call(this);
36
37   this._table = table;
38
39   // init scrollbars
40   this._verScrollBar = new qx.ui.core.ScrollBar(false);
41   this._horScrollBar = new qx.ui.core.ScrollBar(true);
42
43   var scrollBarWidth = this._verScrollBar.getPreferredBoxWidth();
44
45   this._verScrollBar.setWidth("auto");
46   this._horScrollBar.setHeight("auto");
47   this._horScrollBar.setPaddingRight(scrollBarWidth);
48   //this._verScrollBar.setMergeEvents(true);
49
50   this._horScrollBar.addEventListener("changeValue", this._onScrollX, this);
51   this._verScrollBar.addEventListener("changeValue", this._onScrollY, this);
52
53   // init header
54   this._header = this.getTable().getNewTablePaneHeader()(this);
55   this._header.set({ width:"auto", height:"auto" });
56
57   this._headerClipper = new qx.ui.layout.CanvasLayout;
58   this._headerClipper.setDimension("1*", "auto");
59   this._headerClipper.setOverflow("hidden");
60   this._headerClipper.add(this._header);
61
62   this._spacer = new qx.ui.basic.Terminator;
63   this._spacer.setWidth(scrollBarWidth);
64
65   this._top = new qx.ui.layout.HorizontalBoxLayout;
66   this._top.setHeight("auto");
67   this._top.add(this._headerClipper, this._spacer);
68
69   // init pane
70   this._tablePane = this.getTable().getNewTablePane()(this);
71   this._tablePane.set({ width:"auto", height:"auto" });
72
73   this._focusIndicator = new qx.ui.layout.HorizontalBoxLayout;
74   this._focusIndicator.setAppearance("table-focus-indicator");
75   this._focusIndicator.hide();
76
77   // Workaround: If the _focusIndicator has no content if always gets a too
78   //       high hight in IE.
79   var dummyContent = new qx.ui.basic.Terminator;
80   dummyContent.setWidth(0);
81   this._focusIndicator.add(dummyContent);
82
83   this._paneClipper = new qx.ui.layout.CanvasLayout;
84   this._paneClipper.setWidth("1*");
85   this._paneClipper.setOverflow("hidden");
86   this._paneClipper.add(this._tablePane, this._focusIndicator);
87   this._paneClipper.addEventListener("mousewheel", this._onmousewheel, this);
88
89   // add all child widgets
90   var scrollerBody = new qx.ui.layout.HorizontalBoxLayout;
91   scrollerBody.setHeight("1*");
92   scrollerBody.add(this._paneClipper, this._verScrollBar);
93
94   this.add(this._top, scrollerBody, this._horScrollBar);
95
96   // init event handlers
97   this.addEventListener("mousemove", this._onmousemove, this);
98   this.addEventListener("mousedown", this._onmousedown, this);
99   this.addEventListener("mouseup",   this._onmouseup,   this);
100   this.addEventListener("click",     this._onclick,     this);
101   this.addEventListener("dblclick",  this._ondblclick,  this);
102   this.addEventListener("mouseout",  this._onmouseout,  this);
103 });
104
105 /** Whether to show the horizontal scroll bar */
106 qx.OO.addProperty({ name:"horizontalScrollBarVisible", type:"boolean", defaultValue:true });
107
108 /** Whether to show the vertical scroll bar */
109 qx.OO.addProperty({ name:"verticalScrollBarVisible", type:"boolean", defaultValue:true });
110
111 /** The table pane model. */
112 qx.OO.addProperty({ name:"tablePaneModel", type:"object", instance:"qx.ui.table.TablePaneModel" });
113
114 /** The current position of the the horizontal scroll bar. */
115 qx.OO.addProperty({ name:"scrollX", type:"number", allowNull:false, defaultValue:0 });
116
117 /** The current position of the the vertical scroll bar. */
118 qx.OO.addProperty({ name:"scrollY", type:"number", allowNull:false, defaultValue:0 });
119
120 /**
121  * Whether column resize should be live. If false, during resize only a line is
122  * shown and the real resize happens when the user releases the mouse button.
123  */
124 qx.OO.addProperty({ name:"liveResize", type:"boolean", defaultValue:false });
125
126 /**
127  * Whether the focus should moved when the mouse is moved over a cell. If false
128  * the focus is only moved on mouse clicks.
129  */
130 qx.OO.addProperty({ name:"focusCellOnMouseMove", type:"boolean", defaultValue:false });
131
132 /**
133  * Whether to handle selections via the selection manager before setting the
134  * focus.  The traditional behavior is to handle selections after setting the
135  * focus, but setting the focus means redrawing portions of the table, and
136  * some subclasses may want to modify the data to be displayed based on the
137  * selection.
138  */
139 qx.OO.addProperty({ name:"selectBeforeFocus", type:"boolean", defaultValue:false });
140
141
142 // property modifier
143 qx.Proto._modifyHorizontalScrollBarVisible = function(propValue, propOldValue, propData) {
144   // Workaround: We can't use setDisplay, because the scroll bar needs its
145   //       correct height in order to check its value. When using
146   //       setDisplay(false) the height isn't relayouted any more
147   if (propValue) {
148     this._horScrollBar.setHeight("auto");
149   } else {
150     this._horScrollBar.setHeight(0);
151   }
152   this._horScrollBar.setVisibility(propValue);
153
154   // NOTE: We have to flush the queues before updating the content so the new
155   //     layout has been applied and _updateContent is able to work with
156   //     correct values.
157   qx.ui.core.Widget.flushGlobalQueues();
158   this._updateContent();
159
160   return true;
161 }
162
163
164 // property modifier
165 qx.Proto._modifyVerticalScrollBarVisible = function(propValue, propOldValue, propData) {
166   // Workaround: See _modifyHorizontalScrollBarVisible
167   if (propValue) {
168     this._verScrollBar.setWidth("auto");
169   } else {
170     this._verScrollBar.setWidth(0);
171   }
172   this._verScrollBar.setVisibility(propValue);
173
174   var scrollBarWidth = propValue ? this._verScrollBar.getPreferredBoxWidth() : 0;
175   this._horScrollBar.setPaddingRight(scrollBarWidth);
176   this._spacer.setWidth(scrollBarWidth);
177
178   return true;
179 }
180
181
182 // property modifier
183 qx.Proto._modifyTablePaneModel = function(propValue, propOldValue, propData) {
184   if (propOldValue != null) {
185     propOldValue.removeEventListener("modelChanged", this._onPaneModelChanged, this);
186   }
187   propValue.addEventListener("modelChanged", this._onPaneModelChanged, this);
188
189   return true;
190 }
191
192
193 // property modifier
194 qx.Proto._modifyScrollX = function(propValue, propOldValue, propData) {
195   this._horScrollBar.setValue(propValue);
196   return true;
197 }
198
199
200 // property modifier
201 qx.Proto._modifyScrollY = function(propValue, propOldValue, propData) {
202   this._verScrollBar.setValue(propValue);
203   return true;
204 }
205
206
207 /**
208  * Returns the table this scroller belongs to.
209  *
210  * @return {Table} the table.
211  */
212 qx.Proto.getTable = function() {
213   return this._table;
214 };
215
216
217 /**
218  * Event handler. Called when the visibility of a column has changed.
219  *
220  * @param evt {Map} the event.
221  */
222 qx.Proto._onColVisibilityChanged = function(evt) {
223   this._updateHorScrollBarMaximum();
224   this._updateFocusIndicator();
225 }
226
227
228 /**
229  * Event handler. Called when the width of a column has changed.
230  *
231  * @param evt {Map} the event.
232  */
233 qx.Proto._onColWidthChanged = function(evt) {
234   this._header._onColWidthChanged(evt);
235   this._tablePane._onColWidthChanged(evt);
236
237   var data = evt.getData();
238   var paneModel = this.getTablePaneModel();
239   var x = paneModel.getX(data.col);
240   if (x != -1) {
241     // The change was in this scroller
242     this._updateHorScrollBarMaximum();
243     this._updateFocusIndicator();
244   }
245 }
246
247
248 /**
249  * Event handler. Called when the column order has changed.
250  *
251  * @param evt {Map} the event.
252  */
253 qx.Proto._onColOrderChanged = function(evt) {
254   this._header._onColOrderChanged(evt);
255   this._tablePane._onColOrderChanged(evt);
256
257   this._updateHorScrollBarMaximum();
258 }
259
260
261 /**
262  * Event handler. Called when the table model has changed.
263  *
264  * @param evt {Map} the event.
265  */
266 qx.Proto._onTableModelDataChanged = function(evt) {
267   this._tablePane._onTableModelDataChanged(evt);
268
269   var rowCount = this.getTable().getTableModel().getRowCount();
270   if (rowCount != this._lastRowCount) {
271     this._lastRowCount = rowCount;
272
273     this._updateVerScrollBarMaximum();
274     if (this.getFocusedRow() >= rowCount) {
275       if (rowCount == 0) {
276         this.setFocusedCell(null, null);
277       } else {
278         this.setFocusedCell(this.getFocusedColumn(), rowCount - 1);
279       }
280     }
281   }
282 }
283
284
285 /**
286  * Event handler. Called when the selection has changed.
287  *
288  * @param evt {Map} the event.
289  */
290 qx.Proto._onSelectionChanged = function(evt) {
291   this._tablePane._onSelectionChanged(evt);
292 };
293
294
295 /**
296  * Event handler. Called when the table gets or looses the focus.
297  *
298  * @param evt {Map} the event.
299  */
300 qx.Proto._onFocusChanged = function(evt) {
301   this._focusIndicator.setState("tableHasFocus", this.getTable().getFocused());
302
303   this._tablePane._onFocusChanged(evt);
304 };
305
306
307 /**
308  * Event handler. Called when the table model meta data has changed.
309  *
310  * @param evt {Map} the event.
311  */
312 qx.Proto._onTableModelMetaDataChanged = function(evt) {
313   this._header._onTableModelMetaDataChanged(evt);
314   this._tablePane._onTableModelMetaDataChanged(evt);
315 };
316
317
318 /**
319  * Event handler. Called when the pane model has changed.
320  *
321  * @param evt {Map} the event.
322  */
323 qx.Proto._onPaneModelChanged = function(evt) {
324   this._header._onPaneModelChanged(evt);
325   this._tablePane._onPaneModelChanged(evt);
326 };
327
328
329 /**
330  * Updates the maximum of the horizontal scroll bar, so it corresponds to the
331  * total width of the columns in the table pane.
332  */
333 qx.Proto._updateHorScrollBarMaximum = function() {
334   this._horScrollBar.setMaximum(this.getTablePaneModel().getTotalWidth());
335 }
336
337
338 /**
339  * Updates the maximum of the vertical scroll bar, so it corresponds to the
340  * number of rows in the table.
341  */
342 qx.Proto._updateVerScrollBarMaximum = function() {
343   var rowCount = this.getTable().getTableModel().getRowCount();
344   var rowHeight = this.getTable().getRowHeight();
345
346   if (this.getTable().getKeepFirstVisibleRowComplete()) {
347     this._verScrollBar.setMaximum((rowCount + 1) * rowHeight);
348   } else {
349     this._verScrollBar.setMaximum(rowCount * rowHeight);
350   }
351 }
352
353
354 /**
355  * Event handler. Called when the table property "keepFirstVisibleRowComplete"
356  * changed.
357  */
358 qx.Proto._onKeepFirstVisibleRowCompleteChanged = function() {
359   this._updateVerScrollBarMaximum();
360   this._updateContent();
361 };
362
363
364 // overridden
365 qx.Proto._changeInnerHeight = function(newValue, oldValue) {
366   // The height has changed -> Update content
367   this._postponedUpdateContent();
368
369   return qx.ui.layout.VerticalBoxLayout.prototype._changeInnerHeight.call(this, newValue, oldValue);
370 }
371
372
373 // overridden
374 qx.Proto._afterAppear = function() {
375   qx.ui.layout.VerticalBoxLayout.prototype._afterAppear.call(this);
376
377   var self = this;
378   this.getElement().onselectstart = qx.lang.Function.returnFalse;
379
380   this._updateContent();
381   this._header._updateContent();
382   this._updateHorScrollBarMaximum();
383   this._updateVerScrollBarMaximum();
384 }
385
386
387 /**
388  * Event handler. Called when the horizontal scroll bar moved.
389  *
390  * @param evt {Map} the event.
391  */
392 qx.Proto._onScrollX = function(evt) {
393   // Workaround: See _updateContent
394   this._header.setLeft(-evt.getData());
395
396   this._paneClipper.setScrollLeft(evt.getData());
397   this.setScrollX(evt.getData());
398 }
399
400
401 /**
402  * Event handler. Called when the vertical scroll bar moved.
403  *
404  * @param evt {Map} the event.
405  */
406 qx.Proto._onScrollY = function(evt) {
407   this._postponedUpdateContent();
408   this.setScrollY(evt.getData());
409 }
410
411
412 /**
413  * Event handler. Called when the user moved the mouse wheel.
414  *
415  * @param evt {Map} the event.
416  */
417 qx.Proto._onmousewheel = function(evt) {
418   var table = this.getTable();
419
420   if (! table.getEnabled()) {
421     return;
422   }
423
424   this._verScrollBar.setValue(this._verScrollBar.getValue()
425     - evt.getWheelDelta() * table.getRowHeight());
426
427   // Update the focus
428   if (this._lastMousePageX && this.getFocusCellOnMouseMove()) {
429     this._focusCellAtPagePos(this._lastMousePageX, this._lastMousePageY);
430   }
431 }
432
433
434 /**
435  * Event handler. Called when the user moved the mouse.
436  *
437  * @param evt {Map} the event.
438  */
439 qx.Proto._onmousemove = function(evt) {
440   var table = this.getTable();
441
442   if (! table.getEnabled()) {
443     return;
444   }
445
446   var tableModel = table.getTableModel();
447   var columnModel = table.getTableColumnModel();
448
449   var useResizeCursor = false;
450   var mouseOverColumn = null;
451
452   var pageX = evt.getPageX();
453   var pageY = evt.getPageY();
454
455   // Workaround: In onmousewheel the event has wrong coordinates for pageX
456   //       and pageY. So we remember the last move event.
457   this._lastMousePageX = pageX;
458   this._lastMousePageY = pageY;
459
460   if (this._resizeColumn != null) {
461     // We are currently resizing -> Update the position
462     var minColumnWidth = qx.ui.table.TablePaneScroller.MIN_COLUMN_WIDTH;
463     var newWidth = Math.max(minColumnWidth, this._lastResizeWidth + pageX - this._lastResizeMousePageX);
464
465     if (this.getLiveResize()) {
466       columnModel.setColumnWidth(this._resizeColumn, newWidth);
467     } else {
468       this._header.setColumnWidth(this._resizeColumn, newWidth);
469
470       var paneModel = this.getTablePaneModel();
471       this._showResizeLine(paneModel.getColumnLeft(this._resizeColumn) + newWidth);
472     }
473
474     useResizeCursor = true;
475     this._lastResizeMousePageX += newWidth - this._lastResizeWidth;
476     this._lastResizeWidth = newWidth;
477   } else if (this._moveColumn != null) {
478     // We are moving a column
479
480     // Check whether we moved outside the click tolerance so we can start
481     // showing the column move feedback
482     // (showing the column move feedback prevents the onclick event)
483     var clickTolerance = qx.ui.table.TablePaneScroller.CLICK_TOLERANCE;
484     if (this._header.isShowingColumnMoveFeedback()
485       || pageX > this._lastMoveMousePageX + clickTolerance
486       || pageX < this._lastMoveMousePageX - clickTolerance)
487     {
488       this._lastMoveColPos += pageX - this._lastMoveMousePageX;
489
490       this._header.showColumnMoveFeedback(this._moveColumn, this._lastMoveColPos);
491
492       // Get the responsible scroller
493       var targetScroller = this._table.getTablePaneScrollerAtPageX(pageX);
494       if (this._lastMoveTargetScroller && this._lastMoveTargetScroller != targetScroller) {
495         this._lastMoveTargetScroller.hideColumnMoveFeedback();
496       }
497       if (targetScroller != null) {
498         this._lastMoveTargetX = targetScroller.showColumnMoveFeedback(pageX);
499       } else {
500         this._lastMoveTargetX = null;
501       }
502
503       this._lastMoveTargetScroller = targetScroller;
504       this._lastMoveMousePageX = pageX;
505     }
506   } else {
507     // This is a normal mouse move
508     var row = this._getRowForPagePos(pageX, pageY);
509     if (row == -1) {
510       // The mouse is over the header
511       var resizeCol = this._getResizeColumnForPageX(pageX);
512       if (resizeCol != -1) {
513         // The mouse is over a resize region -> Show the right cursor
514         useResizeCursor = true;
515       } else {
516         var col = this._getColumnForPageX(pageX);
517         if (col != null && tableModel.isColumnSortable(col)) {
518           mouseOverColumn = col;
519         }
520       }
521     } else if (row != null) {
522       // The mouse is over the data -> update the focus
523       if (this.getFocusCellOnMouseMove()) {
524         this._focusCellAtPagePos(pageX, pageY);
525       }
526     }
527   }
528
529   // Workaround: Setting the cursor to the right widget doesn't work
530   //this._header.setCursor(useResizeCursor ? "e-resize" : null);
531   this.getTopLevelWidget().setGlobalCursor(useResizeCursor ? qx.ui.table.TablePaneScroller.CURSOR_RESIZE_HORIZONTAL : null);
532
533   this._header.setMouseOverColumn(mouseOverColumn);
534 }
535
536
537 /**
538  * Event handler. Called when the user pressed a mouse button.
539  *
540  * @param evt {Map} the event.
541  */
542 qx.Proto._onmousedown = function(evt) {
543   var table = this.getTable();
544
545   if (! table.getEnabled()) {
546     return;
547   }
548
549   var tableModel = table.getTableModel();
550   var columnModel = table.getTableColumnModel();
551
552   var pageX = evt.getPageX();
553   var pageY = evt.getPageY();
554   var row = this._getRowForPagePos(pageX, pageY);
555   if (row == -1) {
556     // mouse is in header
557     var resizeCol = this._getResizeColumnForPageX(pageX);
558     if (resizeCol != -1) {
559       // The mouse is over a resize region -> Start resizing
560       this._resizeColumn = resizeCol;
561       this._lastResizeMousePageX = pageX;
562       this._lastResizeWidth = columnModel.getColumnWidth(this._resizeColumn);
563       this.setCapture(true);
564     } else {
565       // The mouse is not in a resize region
566       var col = this._getColumnForPageX(pageX);
567       if (col != null) {
568         // Prepare column moving
569         this._moveColumn = col;
570         this._lastMoveMousePageX = pageX;
571         this._lastMoveColPos = this.getTablePaneModel().getColumnLeft(col);
572         this.setCapture(true);
573       }
574     }
575   } else if (row != null) {
576     var selectBeforeFocus = this.getSelectBeforeFocus();
577
578     if (selectBeforeFocus) {
579       table._getSelectionManager().handleMouseDown(row, evt);
580     }
581
582     // The mouse is over the data -> update the focus
583     if (! this.getFocusCellOnMouseMove()) {
584       this._focusCellAtPagePos(pageX, pageY);
585     }
586
587     if (! selectBeforeFocus) {
588       table._getSelectionManager().handleMouseDown(row, evt);
589     }
590   }
591 }
592
593
594 /**
595  * Event handler. Called when the user released a mouse button.
596  *
597  * @param evt {Map} the event.
598  */
599 qx.Proto._onmouseup = function(evt) {
600   var table = this.getTable();
601
602   if (! table.getEnabled()) {
603     return;
604   }
605
606   var columnModel = table.getTableColumnModel();
607   var paneModel = this.getTablePaneModel();
608
609   if (this._resizeColumn != null) {
610     // We are currently resizing -> Finish resizing
611     if (! this.getLiveResize()) {
612       this._hideResizeLine();
613       columnModel.setColumnWidth(this._resizeColumn, this._lastResizeWidth);
614     }
615
616     this._resizeColumn = null;
617     this.setCapture(false);
618
619     this.getTopLevelWidget().setGlobalCursor(null);
620   } else if (this._moveColumn != null) {
621     // We are moving a column -> Drop the column
622     this._header.hideColumnMoveFeedback();
623     if (this._lastMoveTargetScroller) {
624       this._lastMoveTargetScroller.hideColumnMoveFeedback();
625     }
626
627     if (this._lastMoveTargetX != null) {
628       var fromVisXPos = paneModel.getFirstColumnX() + paneModel.getX(this._moveColumn);
629       var toVisXPos = this._lastMoveTargetX;
630       if (toVisXPos != fromVisXPos && toVisXPos != fromVisXPos + 1) {
631         // The column was really moved to another position
632         // (and not moved before or after itself, which is a noop)
633
634         // Translate visible positions to overall positions
635         var fromCol = columnModel.getVisibleColumnAtX(fromVisXPos);
636         var toCol   = columnModel.getVisibleColumnAtX(toVisXPos);
637         var fromOverXPos = columnModel.getOverallX(fromCol);
638         var toOverXPos = (toCol != null) ? columnModel.getOverallX(toCol) : columnModel.getOverallColumnCount();
639
640         if (toOverXPos > fromOverXPos) {
641           // Don't count the column itself
642           toOverXPos--;
643         }
644
645         // Move the column
646         columnModel.moveColumn(fromOverXPos, toOverXPos);
647       }
648     }
649
650     this._moveColumn = null;
651     this._lastMoveTargetX = null;
652     this.setCapture(false);
653   } else {
654     // This is a normal mouse up
655     var row = this._getRowForPagePos(evt.getPageX(), evt.getPageY());
656     if (row != -1 && row != null) {
657       table._getSelectionManager().handleMouseUp(row, evt);
658     }
659   }
660 }
661
662
663 /**
664  * Event handler. Called when the user clicked a mouse button.
665  *
666  * @param evt {Map} the event.
667  */
668 qx.Proto._onclick = function(evt) {
669   var table = this.getTable();
670
671   if (! table.getEnabled()) {
672     return;
673   }
674
675   var tableModel = table.getTableModel();
676
677   var pageX = evt.getPageX();
678   var pageY = evt.getPageY();
679   var row = this._getRowForPagePos(pageX, pageY);
680   if (row == -1) {
681     // mouse is in header
682     var resizeCol = this._getResizeColumnForPageX(pageX);
683     if (resizeCol == -1) {
684       // mouse is not in a resize region
685       var col = this._getColumnForPageX(pageX);
686       if (col != null && tableModel.isColumnSortable(col)) {
687         // Sort that column
688         var sortCol = tableModel.getSortColumnIndex();
689         var ascending = (col != sortCol) ? true : !tableModel.isSortAscending();
690
691         tableModel.sortByColumn(col, ascending);
692         table.getSelectionModel().clearSelection();
693       }
694     }
695   } else if (row != null) {
696     table._getSelectionManager().handleClick(row, evt);
697   }
698 }
699
700
701 /**
702  * Event handler. Called when the user double clicked a mouse button.
703  *
704  * @param evt {Map} the event.
705  */
706 qx.Proto._ondblclick = function(evt) {
707   if (! this.isEditing()) {
708     this._focusCellAtPagePos(evt.getPageX(), evt.getPageY());
709     this.startEditing();
710   }
711 }
712
713
714 /**
715  * Event handler. Called when the mouse moved out.
716  *
717  * @param evt {Map} the event.
718  */
719 qx.Proto._onmouseout = function(evt) {
720   var table = this.getTable();
721
722   if (! table.getEnabled()) {
723     return;
724   }
725
726   /*
727   // Workaround: See _onmousemove
728   this._lastMousePageX = null;
729   this._lastMousePageY = null;
730   */
731
732   // Reset the resize cursor when the mouse leaves the header
733   // If currently a column is resized then do nothing
734   // (the cursor will be reset on mouseup)
735   if (this._resizeColumn == null) {
736     this.getTopLevelWidget().setGlobalCursor(null);
737   }
738
739   this._header.setMouseOverColumn(null);
740 }
741
742
743 /**
744  * Shows the resize line.
745  *
746  * @param x {Integer} the position where to show the line (in pixels, relative to
747  *    the left side of the pane).
748  */
749 qx.Proto._showResizeLine = function(x) {
750   var resizeLine = this._resizeLine;
751   if (resizeLine == null) {
752     resizeLine = new qx.ui.basic.Terminator;
753     resizeLine.setBackgroundColor("#D6D5D9");
754     resizeLine.setWidth(3);
755     this._paneClipper.add(resizeLine);
756     qx.ui.core.Widget.flushGlobalQueues();
757
758     this._resizeLine = resizeLine;
759   }
760
761   resizeLine._applyRuntimeLeft(x - 2); // -1 for the width
762   resizeLine._applyRuntimeHeight(this._paneClipper.getBoxHeight() + this._paneClipper.getScrollTop());
763
764   this._resizeLine.removeStyleProperty("visibility");
765 }
766
767
768 /**
769  * Hides the resize line.
770  */
771 qx.Proto._hideResizeLine = function() {
772   this._resizeLine.setStyleProperty("visibility", "hidden");
773 }
774
775
776 /**
777  * Shows the feedback shown while a column is moved by the user.
778  *
779  * @param pageX {Integer} the x position of the mouse in the page (in pixels).
780  * @return {Integer} the visible x position of the column in the whole table.
781  */
782 qx.Proto.showColumnMoveFeedback = function(pageX) {
783   var paneModel = this.getTablePaneModel();
784   var columnModel = this.getTable().getTableColumnModel();
785   var paneLeftX = qx.html.Location.getClientBoxLeft(this._tablePane.getElement());
786   var colCount = paneModel.getColumnCount();
787
788   var targetXPos = 0;
789   var targetX = 0;
790   var currX = paneLeftX;
791   for (var xPos = 0; xPos < colCount; xPos++) {
792     var col = paneModel.getColumnAtX(xPos);
793     var colWidth = columnModel.getColumnWidth(col);
794
795     if (pageX < currX + colWidth / 2) {
796       break;
797     }
798
799     currX += colWidth;
800     targetXPos = xPos + 1;
801     targetX = currX - paneLeftX;
802   }
803
804   // Ensure targetX is visible
805   var clipperLeftX = qx.html.Location.getClientBoxLeft(this._paneClipper.getElement());
806   var clipperWidth = this._paneClipper.getBoxWidth();
807   var scrollX = clipperLeftX - paneLeftX;
808   // NOTE: +2/-1 because of feedback width
809   targetX = qx.lang.Number.limit(targetX, scrollX + 2, scrollX + clipperWidth - 1);
810
811   this._showResizeLine(targetX);
812
813   // Return the overall target x position
814   return paneModel.getFirstColumnX() + targetXPos;
815 }
816
817
818 /**
819  * Hides the feedback shown while a column is moved by the user.
820  */
821 qx.Proto.hideColumnMoveFeedback = function() {
822   this._hideResizeLine();
823 }
824
825
826 /**
827  * Sets the focus to the cell that's located at the page position
828  * <code>pageX</code>/<code>pageY</code>. If there is no cell at that position,
829  * nothing happens.
830  *
831  * @param pageX {Integer} the x position in the page (in pixels).
832  * @param pageY {Integer} the y position in the page (in pixels).
833  */
834 qx.Proto._focusCellAtPagePos = function(pageX, pageY) {
835   var row = this._getRowForPagePos(pageX, pageY);
836   if (row != -1 && row != null) {
837     // The mouse is over the data -> update the focus
838     var col = this._getColumnForPageX(pageX);
839     if (col != null) {
840       this._table.setFocusedCell(col, row);
841     }
842   }
843 }
844
845
846 /**
847  * Sets the currently focused cell.
848  *
849  * @param col {Integer} the model index of the focused cell's column.
850  * @param row {Integer} the model index of the focused cell's row.
851  */
852 qx.Proto.setFocusedCell = function(col, row) {
853   if (!this.isEditing()) {
854     this._tablePane.setFocusedCell(col, row, this._updateContentPlanned);
855
856     this._focusedCol = col;
857     this._focusedRow = row;
858
859     // Move the focus indicator
860     if (! this._updateContentPlanned) {
861       this._updateFocusIndicator();
862     }
863   }
864 }
865
866
867 /**
868  * Returns the column of currently focused cell.
869  *
870  * @return {Integer} the model index of the focused cell's column.
871  */
872 qx.Proto.getFocusedColumn = function() {
873   return this._focusedCol;
874 };
875
876
877 /**
878  * Returns the row of currently focused cell.
879  *
880  * @return {Integer} the model index of the focused cell's column.
881  */
882 qx.Proto.getFocusedRow = function() {
883   return this._focusedRow;
884 };
885
886
887 /**
888  * Scrolls a cell visible.
889  *
890  * @param col {Integer} the model index of the column the cell belongs to.
891  * @param row {Integer} the model index of the row the cell belongs to.
892  */
893 qx.Proto.scrollCellVisible = function(col, row) {
894   var paneModel = this.getTablePaneModel();
895   var xPos = paneModel.getX(col);
896
897   if (xPos != -1) {
898     var columnModel = this.getTable().getTableColumnModel();
899
900     var colLeft = paneModel.getColumnLeft(col);
901     var colWidth = columnModel.getColumnWidth(col);
902     var rowHeight = this.getTable().getRowHeight();
903     var rowTop = row * rowHeight;
904
905     var scrollX = this.getScrollX();
906     var scrollY = this.getScrollY();
907     var viewWidth = this._paneClipper.getBoxWidth();
908     var viewHeight = this._paneClipper.getBoxHeight();
909
910     // NOTE: We don't use qx.lang.Number.limit, because min should win if max < min
911     var minScrollX = Math.min(colLeft, colLeft + colWidth - viewWidth);
912     var maxScrollX = colLeft;
913     this.setScrollX(Math.max(minScrollX, Math.min(maxScrollX, scrollX)));
914
915     var minScrollY = rowTop + rowHeight - viewHeight;
916     if (this.getTable().getKeepFirstVisibleRowComplete()) {
917       minScrollY += rowHeight - 1;
918     }
919     var maxScrollY = rowTop;
920     this.setScrollY(Math.max(minScrollY, Math.min(maxScrollY, scrollY)));
921   }
922 }
923
924
925 /**
926  * Returns whether currently a cell is editing.
927  *
928  * @return whether currently a cell is editing.
929  */
930 qx.Proto.isEditing = function() {
931   return this._cellEditor != null;
932 }
933
934
935 /**
936  * Starts editing the currently focused cell. Does nothing if already editing
937  * or if the column is not editable.
938  *
939  * @return {Boolean} whether editing was started
940  */
941 qx.Proto.startEditing = function() {
942   var tableModel = this.getTable().getTableModel();
943   var col   = this._focusedCol;
944
945   if (!this.isEditing() && (col != null) && tableModel.isColumnEditable(col)) {
946     var row   = this._focusedRow;
947     var xPos  = this.getTablePaneModel().getX(col);
948     var value = tableModel.getValue(col, row);
949
950     this._cellEditorFactory = this.getTable().getTableColumnModel().getCellEditorFactory(col);
951     var cellInfo = { col:col, row:row, xPos:xPos, value:value }
952     this._cellEditor = this._cellEditorFactory.createCellEditor(cellInfo);
953     this._cellEditor.set({ width:"100%", height:"100%" });
954
955     this._focusIndicator.add(this._cellEditor);
956     this._focusIndicator.addState("editing");
957
958     this._cellEditor.addEventListener("changeFocused", this._onCellEditorFocusChanged, this);
959
960     // Workaround: Calling focus() directly has no effect
961     var editor = this._cellEditor;
962     window.setTimeout(function() {
963       editor.focus();
964     }, 0);
965
966     return true;
967   }
968
969   return false;
970 }
971
972
973 /**
974  * Stops editing and writes the editor's value to the model.
975  */
976 qx.Proto.stopEditing = function() {
977   this.flushEditor();
978   this.cancelEditing();
979 }
980
981
982 /**
983  * Writes the editor's value to the model.
984  */
985 qx.Proto.flushEditor = function() {
986   if (this.isEditing()) {
987     var value = this._cellEditorFactory.getCellEditorValue(this._cellEditor);
988     this.getTable().getTableModel().setValue(this._focusedCol, this._focusedRow, value);
989
990     this._table.focus();
991   }
992 }
993
994
995 /**
996  * Stops editing without writing the editor's value to the model.
997  */
998 qx.Proto.cancelEditing = function() {
999   if (this.isEditing()) {
1000     this._focusIndicator.remove(this._cellEditor);
1001     this._focusIndicator.removeState("editing");
1002     this._cellEditor.dispose();
1003
1004     this._cellEditor.removeEventListener("changeFocused", this._onCellEditorFocusChanged, this);
1005     this._cellEditor = null;
1006     this._cellEditorFactory = null;
1007   }
1008 }
1009
1010
1011 /**
1012  * Event handler. Called when the focused state of the cell editor changed.
1013  *
1014  * @param evt {Map} the event.
1015  */
1016 qx.Proto._onCellEditorFocusChanged = function(evt) {
1017   if (!this._cellEditor.getFocused()) {
1018     this.stopEditing();
1019   }
1020 }
1021
1022
1023 /**
1024  * Returns the model index of the column the mouse is over or null if the mouse
1025  * is not over a column.
1026  *
1027  * @param pageX {Integer} the x position of the mouse in the page (in pixels).
1028  * @return {Integer} the model index of the column the mouse is over.
1029  */
1030 qx.Proto._getColumnForPageX = function(pageX) {
1031   var headerLeftX = qx.html.Location.getClientBoxLeft(this._header.getElement());
1032
1033   var columnModel = this.getTable().getTableColumnModel();
1034   var paneModel = this.getTablePaneModel();
1035   var colCount = paneModel.getColumnCount();
1036   var currX = headerLeftX;
1037   for (var x = 0; x < colCount; x++) {
1038     var col = paneModel.getColumnAtX(x);
1039     var colWidth = columnModel.getColumnWidth(col);
1040     currX += colWidth;
1041
1042     if (pageX < currX) {
1043       return col;
1044     }
1045   }
1046
1047   return null;
1048 }
1049
1050
1051 /**
1052  * Returns the model index of the column that should be resized when dragging
1053  * starts here. Returns -1 if the mouse is in no resize region of any column.
1054  *
1055  * @param pageX {Integer} the x position of the mouse in the page (in pixels).
1056  * @return {Integer} the column index.
1057  */
1058 qx.Proto._getResizeColumnForPageX = function(pageX) {
1059   var headerLeftX = qx.html.Location.getClientBoxLeft(this._header.getElement());
1060
1061   var columnModel = this.getTable().getTableColumnModel();
1062   var paneModel = this.getTablePaneModel();
1063   var colCount = paneModel.getColumnCount();
1064   var currX = headerLeftX;
1065   var regionRadius = qx.ui.table.TablePaneScroller.RESIZE_REGION_RADIUS;
1066   for (var x = 0; x < colCount; x++) {
1067     var col = paneModel.getColumnAtX(x);
1068     var colWidth = columnModel.getColumnWidth(col);
1069     currX += colWidth;
1070
1071     if (pageX >= (currX - regionRadius) && pageX <= (currX + regionRadius)) {
1072       return col;
1073     }
1074   }
1075
1076   return -1;
1077 }
1078
1079
1080 /**
1081  * Returns the model index of the row the mouse is currently over. Returns -1 if
1082  * the mouse is over the header. Returns null if the mouse is not over any
1083  * column.
1084  *
1085  * @param pageX {Integer} the mouse x position in the page.
1086  * @param pageY {Integer} the mouse y position in the page.
1087  * @return {Integer} the model index of the row the mouse is currently over.
1088  */
1089 qx.Proto._getRowForPagePos = function(pageX, pageY) {
1090   var paneClipperElem = this._paneClipper.getElement();
1091   var paneClipperLeftX = qx.html.Location.getClientBoxLeft(paneClipperElem);
1092   var paneClipperRightX = qx.html.Location.getClientBoxRight(paneClipperElem);
1093   if (pageX < paneClipperLeftX || pageX > paneClipperRightX) {
1094     // There was no cell or header cell hit
1095     return null;
1096   }
1097
1098   var paneClipperTopY = qx.html.Location.getClientBoxTop(paneClipperElem);
1099   var paneClipperBottomY = qx.html.Location.getClientBoxBottom(paneClipperElem);
1100   if (pageY >= paneClipperTopY && pageY <= paneClipperBottomY) {
1101     // This event is in the pane -> Get the row
1102     var rowHeight = this.getTable().getRowHeight();
1103
1104     var scrollY = this._verScrollBar.getValue();
1105     if (this.getTable().getKeepFirstVisibleRowComplete()) {
1106       scrollY = Math.floor(scrollY / rowHeight) * rowHeight;
1107     }
1108
1109     var tableY = scrollY + pageY - paneClipperTopY;
1110     var row = Math.floor(tableY / rowHeight);
1111
1112     var rowCount = this.getTable().getTableModel().getRowCount();
1113     return (row < rowCount) ? row : null;
1114   }
1115
1116   var headerElem = this._headerClipper.getElement();
1117   if (pageY >= qx.html.Location.getClientBoxTop(headerElem)
1118     && pageY <= qx.html.Location.getClientBoxBottom(headerElem)
1119     && pageX <= qx.html.Location.getClientBoxRight(headerElem))
1120   {
1121     // This event is in the pane -> Return -1 for the header
1122     return -1;
1123   }
1124
1125   return null;
1126 }
1127
1128
1129 /**
1130  * Sets the widget that should be shown in the top right corner.
1131  * <p>
1132  * The widget will not be disposed, when this table scroller is disposed. So the
1133  * caller has to dispose it.
1134  *
1135  * @param widget {qx.ui.core.Widget} The widget to set. May be null.
1136  */
1137 qx.Proto.setTopRightWidget = function(widget) {
1138   var oldWidget = this._topRightWidget;
1139   if (oldWidget != null) {
1140     this._top.remove(oldWidget);
1141   }
1142
1143   if (widget != null) {
1144     this._top.remove(this._spacer);
1145     this._top.add(widget);
1146   } else if (oldWidget != null) {
1147     this._top.add(this._spacer);
1148   }
1149
1150   this._topRightWidget = widget;
1151 }
1152
1153
1154 /**
1155  * Returns the header.
1156  *
1157  * @return {TablePaneHeader} the header.
1158  */
1159 qx.Proto.getHeader = function() {
1160   return this._header;
1161 }
1162
1163
1164 /**
1165  * Returns the table pane.
1166  *
1167  * @return {TablePane} the table pane.
1168  */
1169 qx.Proto.getTablePane = function() {
1170   return this._tablePane;
1171 }
1172
1173
1174 /**
1175  * Returns which scrollbars are needed.
1176  *
1177  * @param forceHorizontal {Boolean ? false} Whether to show the horizontal
1178  *    scrollbar always.
1179  * @param preventVertical {Boolean ? false} Whether tp show the vertical scrollbar
1180  *    never.
1181  * @return {Integer} which scrollbars are needed. This may be any combination of
1182  *    {@link #HORIZONTAL_SCROLLBAR} or {@link #VERTICAL_SCROLLBAR}
1183  *    (combined by OR).
1184  */
1185 qx.Proto.getNeededScrollBars = function(forceHorizontal, preventVertical) {
1186   var barWidth = this._verScrollBar.getPreferredBoxWidth();
1187
1188   // Get the width and height of the view (without scroll bars)
1189   var viewWidth = this._paneClipper.getInnerWidth();
1190   if (this.getVerticalScrollBarVisible()) {
1191     viewWidth += barWidth;
1192   }
1193   var viewHeight = this._paneClipper.getInnerHeight();
1194   if (this.getHorizontalScrollBarVisible()) {
1195     viewHeight += barWidth;
1196   }
1197
1198   // Get the (virtual) width and height of the pane
1199   var paneWidth = this.getTablePaneModel().getTotalWidth();
1200   var paneHeight = this.getTable().getRowHeight() * this.getTable().getTableModel().getRowCount();
1201
1202   // Check which scrollbars are needed
1203   var horNeeded = false;
1204   var verNeeded = false;
1205   if (paneWidth > viewWidth) {
1206     horNeeded = true;
1207     if (paneHeight > viewHeight - barWidth) {
1208       verNeeded = true;
1209     }
1210   } else if (paneHeight > viewHeight) {
1211     verNeeded = true;
1212     if (!preventVertical && (paneWidth > viewWidth - barWidth)) {
1213       horNeeded = true;
1214     }
1215   }
1216
1217   // Create the mask
1218   var horBar = qx.ui.table.TablePaneScroller.HORIZONTAL_SCROLLBAR;
1219   var verBar = qx.ui.table.TablePaneScroller.VERTICAL_SCROLLBAR;
1220   return ((forceHorizontal || horNeeded) ? horBar : 0)
1221      | ((preventVertical || !verNeeded) ? 0 : verBar);
1222 }
1223
1224
1225 /**
1226  * Does a postponed update of the content.
1227  *
1228  * @see #_updateContent
1229  */
1230 qx.Proto._postponedUpdateContent = function() {
1231   if (! this._updateContentPlanned) {
1232     var self = this;
1233     window.setTimeout(function() {
1234       self._updateContent();
1235       self._updateContentPlanned = false;
1236       qx.ui.core.Widget.flushGlobalQueues();
1237     }, 0);
1238     this._updateContentPlanned = true;
1239   }
1240 }
1241
1242
1243 /**
1244  * Updates the content. Sets the right section the table pane should show and
1245  * does the scrolling.
1246  */
1247 qx.Proto._updateContent = function() {
1248   var paneHeight = this._paneClipper.getInnerHeight();
1249   var scrollX = this._horScrollBar.getValue();
1250   var scrollY = this._verScrollBar.getValue();
1251   var rowHeight = this.getTable().getRowHeight();
1252
1253   var firstRow = Math.floor(scrollY / rowHeight);
1254   var oldFirstRow = this._tablePane.getFirstVisibleRow();
1255   this._tablePane.setFirstVisibleRow(firstRow);
1256
1257   var rowCount = Math.ceil(paneHeight / rowHeight);
1258   var paneOffset = 0;
1259   if (! this.getTable().getKeepFirstVisibleRowComplete()) {
1260     // NOTE: We don't consider paneOffset, because this may cause alternating
1261     //       adding and deleting of one row when scolling. Instead we add one row
1262     //       in every case.
1263     rowCount++;
1264     paneOffset = scrollY % rowHeight;
1265   }
1266   this._tablePane.setVisibleRowCount(rowCount);
1267
1268   if (firstRow != oldFirstRow) {
1269     this._updateFocusIndicator();
1270   }
1271
1272   // Workaround: We can't use scrollLeft for the header because IE
1273   //       automatically scrolls the header back, when a column is
1274   //       resized.
1275   this._header.setLeft(-scrollX);
1276   this._paneClipper.setScrollLeft(scrollX);
1277   this._paneClipper.setScrollTop(paneOffset);
1278
1279   //this.debug("paneHeight:"+paneHeight+",rowHeight:"+rowHeight+",firstRow:"+firstRow+",rowCount:"+rowCount+",paneOffset:"+paneOffset);
1280 }
1281
1282
1283 /**
1284  * Updates the location and the visibility of the focus indicator.
1285  */
1286 qx.Proto._updateFocusIndicator = function() {
1287   var table = this.getTable();
1288
1289   if (! table.getEnabled()) {
1290     return;
1291   }
1292
1293   if (this._focusedCol == null) {
1294     this._focusIndicator.hide();
1295   } else {
1296     var xPos = this.getTablePaneModel().getX(this._focusedCol);
1297     if (xPos == -1) {
1298       this._focusIndicator.hide();
1299     } else {
1300       var columnModel = table.getTableColumnModel();
1301       var paneModel = this.getTablePaneModel();
1302
1303       var firstRow = this._tablePane.getFirstVisibleRow();
1304       var rowHeight = table.getRowHeight();
1305
1306       this._focusIndicator.setHeight(rowHeight + 3);
1307       this._focusIndicator.setWidth(columnModel.getColumnWidth(this._focusedCol) + 3);
1308       this._focusIndicator.setTop((this._focusedRow - firstRow) * rowHeight - 2);
1309       this._focusIndicator.setLeft(paneModel.getColumnLeft(this._focusedCol) - 2);
1310
1311       this._focusIndicator.show();
1312
1313       // Force redisplay of the focus indicator right away.  Without this, it
1314       // waits until the mouse stops moving for a while before updating, and
1315       // appears as if it is slow to respond.
1316       qx.ui.core.Widget.flushGlobalQueues();
1317     }
1318   }
1319 }
1320
1321
1322 // overridden
1323 qx.Proto.dispose = function() {
1324   if (this.getDisposed()) {
1325     return true;
1326   }
1327
1328   if (this.getElement() != null) {
1329     this.getElement().onselectstart = null;
1330   }
1331
1332   this._verScrollBar.dispose();
1333   this._horScrollBar.dispose();
1334   this._header.dispose();
1335   this._headerClipper.dispose();
1336   this._spacer.dispose();
1337   this._top.dispose();
1338   this._tablePane.dispose();
1339   this._paneClipper.dispose();
1340
1341   if (this._resizeLine != null) {
1342     this._resizeLine.dispose();
1343   }
1344
1345   this.removeEventListener("mousemove", this._onmousemove, this);
1346   this.removeEventListener("mousedown", this._onmousedown, this);
1347   this.removeEventListener("mouseup", this._onmouseup, this);
1348   this.removeEventListener("click", this._onclick, this);
1349   this.removeEventListener("dblclick", this._ondblclick, this);
1350   this.removeEventListener("mouseout", this._onmouseout, this);
1351
1352   var tablePaneModel = this.getTablePaneModel();
1353   if (tablePaneModel != null) {
1354     tablePaneModel.removeEventListener("modelChanged", this._onPaneModelChanged, this);
1355   }
1356
1357   return qx.ui.layout.VerticalBoxLayout.prototype.dispose.call(this);
1358 }
1359
1360
1361 /** {int} The minimum width a colum could get in pixels. */
1362 qx.Class.MIN_COLUMN_WIDTH = 10;
1363
1364 /** {int} The radius of the resize region in pixels. */
1365 qx.Class.RESIZE_REGION_RADIUS = 5;
1366
1367 /**
1368  * (int) The number of pixels the mouse may move between mouse down and mouse up
1369  * in order to count as a click.
1370  */
1371 qx.Class.CLICK_TOLERANCE = 5;
1372
1373 /**
1374  * (int) The mask for the horizontal scroll bar.
1375  * May be combined with {@link #VERTICAL_SCROLLBAR}.
1376  *
1377  * @see #getNeededScrollBars
1378  */
1379 qx.Class.HORIZONTAL_SCROLLBAR = 1;
1380
1381 /**
1382  * (int) The mask for the vertical scroll bar.
1383  * May be combined with {@link #HORIZONTAL_SCROLLBAR}.
1384  *
1385  * @see #getNeededScrollBars
1386  */
1387 qx.Class.VERTICAL_SCROLLBAR = 2;
1388
1389 /**
1390  * (string) The correct value for the CSS style attribute "cursor" for the
1391  * horizontal resize cursor.
1392  */
1393 qx.Class.CURSOR_RESIZE_HORIZONTAL = (qx.core.Client.getInstance().isGecko() && (qx.core.Client.getInstance().getMajor() > 1 || qx.core.Client.getInstance().getMinor() >= 8)) ? "ew-resize" : "e-resize";