1 // Written in the D programming language.
2 
3 /**
4 
5 This module contains implementation of grid widgets
6 
7 
8 GridWidgetBase - abstract grid widget
9 
10 StringGridWidget - grid of strings
11 
12 
13 Synopsis:
14 
15 ----
16 import dlangui.widgets.grid;
17 
18 StringGridWidget grid = new StringGridWidget("GRID1");
19 grid.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
20 grid.showColHeaders = true;
21 grid.showRowHeaders = true;
22 grid.resize(30, 50);
23 grid.fixedCols = 3;
24 grid.fixedRows = 2;
25 //grid.rowSelect = true; // testing full row selection
26 grid.selectCell(4, 6, false);
27 // create sample grid content
28 for (int y = 0; y < grid.rows; y++) {
29     for (int x = 0; x < grid.cols; x++) {
30         grid.setCellText(x, y, "cell("d ~ to!dstring(x + 1) ~ ","d ~ to!dstring(y + 1) ~ ")"d);
31     }
32     grid.setRowTitle(y, to!dstring(y + 1));
33 }
34 for (int x = 0; x < grid.cols; x++) {
35     int col = x + 1;
36     dstring res;
37     int n1 = col / 26;
38     int n2 = col % 26;
39     if (n1)
40         res ~= n1 + 'A';
41     res ~= n2 + 'A';
42     grid.setColTitle(x, res);
43 }
44 grid.autoFit();
45 
46 
47 ----
48 
49 Copyright: Vadim Lopatin, 2014
50 License:   Boost License 1.0
51 Authors:   Vadim Lopatin, coolreader.org@gmail.com
52 */
53 module dlangui.widgets.grid;
54 
55 import dlangui.core.config;
56 import dlangui.widgets.widget;
57 import dlangui.widgets.controls;
58 import dlangui.widgets.scroll;
59 import dlangui.widgets.menu;
60 import std.conv;
61 import std.container.rbtree;
62 import std.algorithm : equal;
63 
64 /// cellPopupMenu signal handler interface
65 interface CellPopupMenuHandler {
66     MenuItem getCellPopupMenu(GridWidgetBase source, int col, int row);
67 }
68 
69 /**
70  * Data provider for GridWidget.
71  */
72 interface GridAdapter {
73     /// number of columns
74     @property int cols();
75     /// number of rows
76     @property int rows();
77     /// returns widget to draw cell at (col, row)
78     Widget cellWidget(int col, int row);
79     /// returns row header widget, null if no header
80     Widget rowHeader(int row);
81     /// returns column header widget, null if no header
82     Widget colHeader(int col);
83 }
84 
85 /**
86  */
87 class StringGridAdapter : GridAdapter {
88     protected int _cols;
89     protected int _rows;
90     protected dstring[][] _data;
91     protected dstring[] _rowTitles;
92     protected dstring[] _colTitles;
93     /// number of columns
94     @property int cols() { return _cols; }
95     /// number of columns
96     @property void cols(int v) { resize(v, _rows); }
97     /// number of rows
98     @property int rows() { return _rows; }
99     /// number of columns
100     @property void rows(int v) { resize(_cols, v); }
101 
102     /// returns row header title
103     dstring rowTitle(int row) {
104         return _rowTitles[row];
105     }
106     /// set row header title
107     StringGridAdapter setRowTitle(int row, dstring title) {
108         _rowTitles[row] = title;
109         return this;
110     }
111     /// returns row header title
112     dstring colTitle(int col) {
113         return _colTitles[col];
114     }
115     /// set col header title
116     StringGridAdapter setColTitle(int col, dstring title) {
117         _colTitles[col] = title;
118         return this;
119     }
120     /// get cell text
121     dstring cellText(int col, int row) {
122         return _data[row][col];
123     }
124     /// set cell text
125     StringGridAdapter setCellText(int col, int row, dstring text) {
126         _data[row][col] = text;
127         return this;
128     }
129     /// set new size
130     void resize(int cols, int rows) {
131         if (cols == _cols && rows == _rows)
132             return;
133         _cols = cols;
134         _rows = rows;
135         _data.length = _rows;
136         for (int y = 0; y < _rows; y++)
137             _data[y].length = _cols;
138         _colTitles.length = _cols;
139         _rowTitles.length = _rows;
140     }
141     /// returns widget to draw cell at (col, row)
142     Widget cellWidget(int col, int row) { return null; }
143     /// returns row header widget, null if no header
144     Widget rowHeader(int row) { return null; }
145     /// returns column header widget, null if no header
146     Widget colHeader(int col) { return null; }
147 }
148 
149 /// grid control action codes
150 enum GridActions : int {
151     /// no action
152     None = 0,
153     /// move selection up
154     Up = 2000,
155     /// expend selection up
156     SelectUp,
157     /// move selection down
158     Down,
159     /// expend selection down
160     SelectDown,
161     /// move selection left
162     Left,
163     /// expend selection left
164     SelectLeft,
165     /// move selection right
166     Right,
167     /// expend selection right
168     SelectRight,
169 
170     /// scroll up, w/o changing selection
171     ScrollUp,
172     /// scroll down, w/o changing selection
173     ScrollDown,
174     /// scroll left, w/o changing selection
175     ScrollLeft,
176     /// scroll right, w/o changing selection
177     ScrollRight,
178 
179     /// scroll up, w/o changing selection
180     ScrollPageUp,
181     /// scroll down, w/o changing selection
182     ScrollPageDown,
183     /// scroll left, w/o changing selection
184     ScrollPageLeft,
185     /// scroll right, w/o changing selection
186     ScrollPageRight,
187 
188     /// move cursor one page up
189     PageUp,
190     /// move cursor one page up with selection
191     SelectPageUp,
192     /// move cursor one page down
193     PageDown,
194     /// move cursor one page down with selection
195     SelectPageDown,
196     /// move cursor to the beginning of page
197     PageBegin, 
198     /// move cursor to the beginning of page with selection
199     SelectPageBegin, 
200     /// move cursor to the end of page
201     PageEnd,   
202     /// move cursor to the end of page with selection
203     SelectPageEnd,   
204     /// move cursor to the beginning of line
205     LineBegin,
206     /// move cursor to the beginning of line with selection
207     SelectLineBegin,
208     /// move cursor to the end of line
209     LineEnd,
210     /// move cursor to the end of line with selection
211     SelectLineEnd,
212     /// move cursor to the beginning of document
213     DocumentBegin,
214     /// move cursor to the beginning of document with selection
215     SelectDocumentBegin,
216     /// move cursor to the end of document
217     DocumentEnd,
218     /// move cursor to the end of document with selection
219     SelectDocumentEnd,
220     /// select all entries without moving the cursor
221     SelectAll,
222     /// Enter key pressed on cell
223     ActivateCell,
224 }
225 
226 /// Adapter for custom drawing of some cells in grid widgets
227 interface CustomGridCellAdapter {
228     /// return true for custom drawn cell
229     bool isCustomCell(int col, int row);
230     /// return cell size
231     Point measureCell(int col, int row);
232     /// draw data cell content
233     void drawCell(DrawBuf buf, Rect rc, int col, int row);
234 }
235 
236 interface GridModelAdapter {
237     @property int fixedCols();
238     @property int fixedRows();
239     @property void fixedCols(int value);
240     @property void fixedRows(int value);
241 }
242 
243 /// Callback for handling of cell selection
244 interface CellSelectedHandler {
245     void onCellSelected(GridWidgetBase source, int col, int row);
246 }
247 
248 /// Callback for handling of cell double click or Enter key press
249 interface CellActivatedHandler {
250     void onCellActivated(GridWidgetBase source, int col, int row);
251 }
252 
253 /// Callback for handling of view scroll (top left visible cell change)
254 interface ViewScrolledHandler {
255     void onViewScrolled(GridWidgetBase source, int col, int row);
256 }
257 
258 /// Abstract grid widget
259 class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler {
260     /// Callback to handle selection change
261     Listener!CellSelectedHandler cellSelected;
262 
263     /// Callback to handle cell double click
264     Listener!CellActivatedHandler cellActivated;
265 
266     /// Callback for handling of view scroll (top left visible cell change)
267     Listener!ViewScrolledHandler viewScrolled;
268 
269     protected CustomGridCellAdapter _customCellAdapter;
270 
271     /// Get adapter to override drawing of some particular cells
272     @property CustomGridCellAdapter customCellAdapter() { return _customCellAdapter; }
273     /// Set adapter to override drawing of some particular cells
274     @property GridWidgetBase customCellAdapter(CustomGridCellAdapter adapter) { _customCellAdapter = adapter; return this; }
275 
276     protected GridModelAdapter _gridModelAdapter;
277     /// Get adapter to hold grid model data
278     @property GridModelAdapter gridModelAdapter() { return _gridModelAdapter; }
279     /// Set adapter to hold grid model data
280     @property GridWidgetBase gridModelAdapter(GridModelAdapter adapter) { _gridModelAdapter = adapter; return this; }
281 
282     protected bool _smoothHScroll = true;
283     /// Get smooth horizontal scroll flag - when true - scrolling by pixels, when false - by cells
284     @property bool smoothHScroll() { return _smoothHScroll; }
285     /// Get smooth horizontal scroll flag - when true - scrolling by pixels, when false - by cells
286     @property GridWidgetBase smoothHScroll(bool flgSmoothScroll) { 
287         if (_smoothHScroll != flgSmoothScroll) {
288             _smoothHScroll = flgSmoothScroll;
289             // TODO: snap to grid if necessary
290             updateScrollBars();
291         }
292         return this;
293     }
294 
295     protected bool _smoothVScroll = true;
296     /// Get smooth vertical scroll flag - when true - scrolling by pixels, when false - by cells
297     @property bool smoothVScroll() { return _smoothVScroll; }
298     /// Get smooth vertical scroll flag - when true - scrolling by pixels, when false - by cells
299     @property GridWidgetBase smoothVScroll(bool flgSmoothScroll) { 
300         if (_smoothVScroll != flgSmoothScroll) {
301             _smoothVScroll = flgSmoothScroll;
302             // TODO: snap to grid if necessary
303             updateScrollBars();
304         }
305         return this;
306     }
307 
308     /// column count (including header columns and fixed columns)
309     protected int _cols;
310     /// row count (including header rows and fixed rows)
311     protected int _rows;
312     /// column widths
313     protected int[] _colWidths;
314     /// total width from first column to right of this
315     protected int[] _colCumulativeWidths;
316     /// row heights
317     protected int[] _rowHeights;
318     /// total height from first row to bottom of this
319     protected int[] _rowCumulativeHeights;
320     /// when true, shows col headers row
321     protected bool _showColHeaders;
322     /// when true, shows row headers column
323     protected bool _showRowHeaders;
324     /// number of header rows (e.g. like col name A, B, C... in excel; 0 for no header row)
325     protected int _headerRows;
326     /// number of header columns (e.g. like row number in excel; 0 for no header column)
327     protected int _headerCols;
328     /// number of fixed (non-scrollable) columns
329     protected int _fixedCols;
330     /// number of fixed (non-scrollable) rows
331     protected int _fixedRows;
332 
333     /// scroll X offset in pixels
334     protected int _scrollX;
335     /// scroll Y offset in pixels
336     protected int _scrollY;
337 
338     /// selected cells when multiselect is enabled
339     protected RedBlackTree!Point _selection;
340     /// selected cell column
341     protected int _col;
342     /// selected cell row
343     protected int _row;
344     /// when true, allows multi cell selection
345     protected bool _multiSelect;
346     private Point _lastSelectedCell;
347     /// when true, allows to select only whole row
348     protected bool _rowSelect;
349     /// default column width - for newly added columns
350     protected int _defColumnWidth;
351     /// default row height - for newly added rows
352     protected int _defRowHeight;
353 
354     // properties
355 
356     /// selected cells when multiselect is enabled
357     @property RedBlackTree!Point selection() { return _selection; }
358     /// selected column
359     @property int col() { return _col - _headerCols; }
360     /// selected row
361     @property int row() { return _row - _headerRows; }
362     /// column count
363     @property int cols() { return _cols - _headerCols; }
364     /// set column count
365     @property GridWidgetBase cols(int c) { resize(c, rows); return this; }
366     /// row count
367     @property int rows() { return _rows - _headerRows; }
368     /// set row count
369     @property GridWidgetBase rows(int r) { resize(cols, r); return this; }
370 
371     protected bool _allowColResizing = true;
372     /// get col resizing flag; when true, allow resizing of column with mouse
373     @property bool allowColResizing() { return _allowColResizing; }
374     /// set col resizing flag; when true, allow resizing of column with mouse
375     @property GridWidgetBase allowColResizing(bool flgAllowColResizing) { _allowColResizing = flgAllowColResizing; return this; }
376 
377     protected uint _selectionColor = 0x804040FF;
378     protected uint _selectionColorRowSelect = 0xC0A0B0FF;
379     protected uint _fixedCellBackgroundColor = 0xC0E0E0E0;
380     protected uint _fixedCellBorderColor = 0xC0C0C0C0;
381     protected uint _cellBorderColor = 0xC0C0C0C0;
382     protected uint _cellHeaderBorderColor = 0xC0202020;
383     protected uint _cellHeaderBackgroundColor = 0xC0909090;
384     protected uint _cellHeaderSelectedBackgroundColor = 0x80FFC040;
385     protected DrawableRef _cellHeaderBackgroundDrawable;
386     protected DrawableRef _cellHeaderSelectedBackgroundDrawable;
387     protected DrawableRef _cellRowHeaderBackgroundDrawable;
388     protected DrawableRef _cellRowHeaderSelectedBackgroundDrawable;
389 
390     /// row header column count
391     @property int headerCols() { return _headerCols; }
392     @property GridWidgetBase headerCols(int c) { 
393         _headerCols = c; 
394         invalidate(); 
395         return this; 
396     }
397     /// col header row count
398     @property int headerRows() { return _headerRows; }
399     @property GridWidgetBase headerRows(int r) { 
400         _headerRows = r; 
401         invalidate(); 
402         return this; 
403     }
404 
405     /// fixed (non-scrollable) data column count
406     @property int fixedCols() { return _gridModelAdapter is null ? _fixedCols : _gridModelAdapter.fixedCols; }
407     @property void fixedCols(int c) { 
408         if (_gridModelAdapter is null)
409             _fixedCols = c;
410         else
411             _gridModelAdapter.fixedCols = c;
412         invalidate(); 
413     }
414     /// fixed (non-scrollable) data row count
415     @property int fixedRows() { return _gridModelAdapter is null ? _fixedRows : _gridModelAdapter.fixedCols; }
416     @property void fixedRows(int r) {
417         if (_gridModelAdapter is null)
418             _fixedRows = r; 
419         else
420             _gridModelAdapter.fixedCols = r;
421         invalidate(); 
422     }
423 
424     /// default column width - for newly added columns
425     @property int defColumnWidth() {
426         return _defColumnWidth;
427     }
428     @property GridWidgetBase defColumnWidth(int v) {
429         _defColumnWidth = v;
430         _changedSize = true;
431         return this;
432     }
433     /// default row height - for newly added rows
434     @property int defRowHeight() {
435         return _defRowHeight;
436     }
437     @property GridWidgetBase defRowHeight(int v) {
438         _defRowHeight = v;
439         _changedSize = true;
440         return this;
441     }
442 
443     /// when true, allows multi cell selection
444     @property bool multiSelect() {
445         return _multiSelect;
446     }
447     @property GridWidgetBase multiSelect(bool flg) {
448         _multiSelect = flg;
449         if (!_multiSelect) {
450             _selection.clear();
451             _selection.insert(Point(_col - _headerCols, _row - _headerRows));
452         }
453         return this;
454     }
455 
456     /// when true, allows only select the whole row
457     @property bool rowSelect() {
458         return _rowSelect;
459     }
460     @property GridWidgetBase rowSelect(bool flg) {
461         _rowSelect = flg;
462         if (_rowSelect) {
463             _selection.clear();
464             _selection.insert(Point(_col - _headerCols, _row - _headerRows));
465         }
466         invalidate();
467         return this;
468     }
469 
470     /// set bool property value, for ML loaders
471     mixin(generatePropertySettersMethodOverride("setBoolProperty", "bool",
472           "showColHeaders", "showColHeaders", "rowSelect", "smoothHScroll", "smoothVScroll", "allowColResizing"));
473 
474     /// set int property value, for ML loaders
475     mixin(generatePropertySettersMethodOverride("setIntProperty", "int",
476           "headerCols", "headerRows", "fixedCols", "fixedRows", "cols", "rows", "defColumnWidth", "defRowHeight"));
477 
478     /// flag to enable column headers
479     @property bool showColHeaders() {
480         return _showColHeaders;
481     }
482 
483     @property GridWidgetBase showColHeaders(bool show) {
484         if (_showColHeaders != show) {
485             _showColHeaders = show;
486             for (int i = 0; i < _headerRows; i++)
487                 autoFitRowHeight(i);
488             invalidate();
489             _changedSize = true;
490         }
491         return this;
492     }
493 
494     /// flag to enable row headers
495     @property bool showRowHeaders() {
496         return _showRowHeaders;
497     }
498 
499     @property GridWidgetBase showRowHeaders(bool show) {
500         if (_showRowHeaders != show) {
501             _showRowHeaders = show;
502             for (int i = 0; i < _headerCols; i++)
503                 autoFitColumnWidth(i);
504             _changedSize = true;
505             invalidate();
506         }
507         return this;
508     }
509 
510     protected bool _changedSize = true;
511     /// recalculate colCumulativeWidths, rowCumulativeHeights after resizes
512     protected void updateCumulativeSizes() {
513         if (!_changedSize)
514             return;
515         _changedSize = false;
516         _colCumulativeWidths.length = _colWidths.length;
517         _rowCumulativeHeights.length = _rowHeights.length;
518         for (int i = 0; i < _colCumulativeWidths.length; i++) {
519             if (i == 0)
520                 _colCumulativeWidths[i] = _colWidths[i];
521             else
522                 _colCumulativeWidths[i] = _colWidths[i] + _colCumulativeWidths[i - 1];
523         }
524         for (int i = 0; i < _rowCumulativeHeights.length; i++) {
525             if (i == 0)
526                 _rowCumulativeHeights[i] = _rowHeights[i];
527             else
528                 _rowCumulativeHeights[i] = _rowHeights[i] + _rowCumulativeHeights[i - 1];
529         }
530     }
531 
532     /// set new size
533     void resize(int c, int r) {
534         if (c == cols && r == rows)
535             return;
536         _changedSize = true;
537         _colWidths.length = c + _headerCols;
538         for (int i = _cols; i < c + _headerCols; i++) {
539             _colWidths[i] = _defColumnWidth;
540         }
541         _rowHeights.length = r + _headerRows;
542         for (int i = _rows; i < r + _headerRows; i++) {
543             _rowHeights[i] = _defRowHeight;
544         }
545         _cols = c + _headerCols;
546         _rows = r + _headerRows;
547         updateCumulativeSizes();
548     }
549 
550     /// count of non-scrollable columns (header + fixed)
551     @property int nonScrollCols() { return _headerCols + fixedCols; }
552     /// count of non-scrollable rows (header + fixed)
553     @property int nonScrollRows() { return _headerRows + fixedRows; }
554     /// return all (fixed + scrollable) cells size in pixels
555     @property Point fullAreaPixels() {
556         if (_changedSize) updateCumulativeSizes();
557         return Point(_cols ? _colCumulativeWidths[_cols - 1] : 0, _rows ? _rowCumulativeHeights[_rows - 1] : 0);
558     }
559     /// non-scrollable area size in pixels
560     @property Point nonScrollAreaPixels() {
561         if (_changedSize) updateCumulativeSizes();
562         int nscols = nonScrollCols;
563         int nsrows = nonScrollRows;
564         return Point(nscols ? _colCumulativeWidths[nscols - 1] : 0, nsrows ? _rowCumulativeHeights[nsrows - 1] : 0);
565     }
566     /// scrollable area size in pixels
567     @property Point scrollAreaPixels() {
568         return fullAreaPixels - nonScrollAreaPixels;
569     }
570     /// get cell rectangle (relative to client area) not counting scroll position
571     Rect cellRectNoScroll(int x, int y) {
572         if (_changedSize) updateCumulativeSizes();
573         if (x < 0 || y < 0 || x >= _cols || y >= _rows)
574             return Rect(0,0,0,0);
575         return Rect(x ? _colCumulativeWidths[x - 1] : 0, y ? _rowCumulativeHeights[y - 1] : 0,
576                 _colCumulativeWidths[x], _rowCumulativeHeights[y]);
577     }
578     /// get cell rectangle moved by scroll pixels (may overlap non-scroll cols!)
579     Rect cellRectScroll(int x, int y) {
580         Rect rc = cellRectNoScroll(x, y);
581         int nscols = nonScrollCols;
582         int nsrows = nonScrollRows;
583         if (x >= nscols) {
584             rc.left -= _scrollX;
585             rc.right -= _scrollX;
586         }
587         if (y >= nsrows) {
588             rc.top -= _scrollY;
589             rc.bottom -= _scrollY;
590         }
591         return rc;
592     }
593     /// returns true if column is inside client area and not overlapped outside scroll area
594     bool colVisible(int x) {
595         if (_changedSize) updateCumulativeSizes();
596         if (x < 0 || x >= _cols)
597             return false;
598         if (x == 0)
599             return true;
600         int nscols = nonScrollCols;
601         if (x < nscols) {
602             // non-scrollable
603             return _colCumulativeWidths[x - 1] < _clientRect.width;
604         } else {
605             // scrollable
606             int start = _colCumulativeWidths[x - 1] - _scrollX;
607             int end = _colCumulativeWidths[x] - _scrollX;
608             if (start >= _clientRect.width)
609                 return false; // at right
610             if (end <= (nscols ? _colCumulativeWidths[nscols - 1] : 0))
611                 return false; // at left
612             return true; // visible
613         }
614     }
615     /// returns true if row is inside client area and not overlapped outside scroll area
616     bool rowVisible(int y) {
617         if (y < 0 || y >= _rows)
618             return false;
619         if (_changedSize) updateCumulativeSizes();
620         if (y == 0)
621             return true; // first row always visible
622         int nsrows = nonScrollRows;
623         if (y < nsrows) {
624             // non-scrollable
625             return _rowCumulativeHeights[y - 1] < _clientRect.height;
626         } else {
627             // scrollable
628             int start = _rowCumulativeHeights[y - 1] - _scrollY;
629             int end = _rowCumulativeHeights[y] - _scrollY;
630             if (start >= _clientRect.height)
631                 return false; // at right
632             if (end <= (nsrows ? _rowCumulativeHeights[nsrows - 1] : 0))
633                 return false; // at left
634             return true; // visible
635         }
636     }
637 
638     void setColWidth(int x, int w) {
639         _colWidths[x] = w;
640         _changedSize = true;
641     }
642 
643     void setRowHeight(int y, int w) {
644         _rowHeights[y] = w;
645         _changedSize = true;
646     }
647 
648     /// get column width, 0 is header column
649     int colWidth(int col) {
650         if (col < 0 || col >= _colWidths.length)
651             return 0;
652         return _colWidths[col];
653     }
654 
655     /// get row height, 0 is header row
656     int rowHeight(int row) {
657         if (row < 0 || row >= _rowHeights.length)
658             return 0;
659         return _rowHeights[row];
660     }
661 
662     /// returns cell rectangle relative to client area; row 0 is col headers row; col 0 is row headers column
663     Rect cellRect(int x, int y) {
664         return cellRectScroll(x, y);
665     }
666 
667     /// converts client rect relative coordinates to cell coordinates
668     bool pointToCell(int x, int y, ref int col, ref int row, ref Rect cellRect) {
669         if (_changedSize) updateCumulativeSizes();
670         int nscols = nonScrollCols;
671         int nsrows = nonScrollRows;
672         Point ns = nonScrollAreaPixels;
673         col = colByAbsoluteX(x < ns.x ? x : x + _scrollX);
674         row = rowByAbsoluteY(y < ns.y ? y : y + _scrollY);
675         cellRect = cellRectScroll(col, row);
676         return cellRect.isPointInside(x, y);
677     }
678 
679     /// update scrollbar positions
680     override protected void updateScrollBars() {
681         if (_changedSize) updateCumulativeSizes();
682         calcScrollableAreaPos();
683         correctScrollPos();
684         super.updateScrollBars();
685     }
686 
687     /// search for index of position inside cumulative sizes array
688     protected static int findPosIndex(int[] cumulativeSizes, int pos) {
689         // binary search
690         if (pos < 0 || !cumulativeSizes.length)
691             return 0;
692         int a = 0; // inclusive lower bound
693         int b = cast(int)cumulativeSizes.length; // exclusive upper bound
694         if (pos >= cumulativeSizes[$ - 1])
695             return b - 1;
696         int * w = cumulativeSizes.ptr;
697         for(;;) {
698             if (a + 1 >= b)
699                 return a; // single point
700             // middle point
701             // always inside range
702             int c = (a + b) >> 1;
703             int start = c ? w[c - 1] : 0;
704             int end = w[c];
705             if (pos < start) {
706                 // left
707                 b = c;
708             } else if (pos >= end) {
709                 // right
710                 a = c + 1;
711             } else {
712                 // found
713                 return c;
714             }
715         }
716     }
717 
718     /// column by X, ignoring scroll position
719     protected int colByAbsoluteX(int x) {
720         if (_changedSize) updateCumulativeSizes();
721         return findPosIndex(_colCumulativeWidths, x);
722     }
723 
724     /// row by Y, ignoring scroll position
725     protected int rowByAbsoluteY(int y) {
726         if (_changedSize) updateCumulativeSizes();
727         return findPosIndex(_rowCumulativeHeights, y);
728     }
729 
730     /// returns first fully visible column in scroll area
731     protected int scrollCol() {
732         if (_changedSize) updateCumulativeSizes();
733         int x = nonScrollAreaPixels.x + _scrollX;
734         int col = colByAbsoluteX(x);
735         int start = col ? _colCumulativeWidths[col - 1] : 0;
736         int end = _colCumulativeWidths[col];
737         if (x <= start)
738             return col;
739         // align to next col
740         return colByAbsoluteX(end);
741     }
742 
743     /// returns last fully visible column in scroll area
744     protected int lastScrollCol() {
745         if (_changedSize) updateCumulativeSizes();
746         int x = nonScrollAreaPixels.x + _scrollX + _visibleScrollableArea.width - 1;
747         int col = colByAbsoluteX(x);
748         int start = col ? _colCumulativeWidths[col - 1] : 0;
749         int end = _colCumulativeWidths[col];
750         if (x >= end - 1) // fully visible
751             return col;
752         if (col > nonScrollCols && col > scrollCol)
753             col--;
754         return col;
755     }
756 
757     /// returns first fully visible row in scroll area
758     protected int scrollRow() {
759         if (_changedSize) updateCumulativeSizes();
760         int y = nonScrollAreaPixels.y + _scrollY;
761         int row = rowByAbsoluteY(y);
762         int start = row ? _rowCumulativeHeights[row - 1] : 0;
763         int end = _rowCumulativeHeights[row];
764         if (y <= start)
765             return row;
766         // align to next col
767         return rowByAbsoluteY(end);
768     }
769 
770     /// returns last fully visible row in scroll area
771     protected int lastScrollRow() {
772         if (_changedSize) updateCumulativeSizes();
773         int y = nonScrollAreaPixels.y + _scrollY + _visibleScrollableArea.height - 1;
774         int row = rowByAbsoluteY(y);
775         int start = row ? _rowCumulativeHeights[row - 1] : 0;
776         int end = _rowCumulativeHeights[row];
777         if (y >= end - 1) // fully visible
778             return row;
779         if (row > nonScrollRows && row > scrollRow)
780             row--;
781         return row;
782     }
783 
784     /// move scroll position horizontally by dx, and vertically by dy; returns true if scrolled
785     bool scrollBy(int dx, int dy) {
786         if (_changedSize) updateCumulativeSizes();
787         int col = scrollCol + dx;
788         int row = scrollRow + dy;
789         if (col >= _cols)
790             col = _cols - 1;
791         if (row >= _rows)
792             row = _rows - 1;
793         if (col < nonScrollCols)
794             col = nonScrollCols;
795         if (row < nonScrollCols)
796             row = nonScrollRows;
797         Rect rc = cellRectNoScroll(col, row);
798         Point ns = nonScrollAreaPixels;
799         return scrollTo(rc.left - ns.x, rc.top - ns.y);
800     }
801 
802     /// override to support modification of client rect after change, e.g. apply offset
803     override protected void handleClientRectLayout(ref Rect rc) {
804         //correctScrollPos();
805     }
806 
807     // ensure scroll position is inside min/max area
808     protected void correctScrollPos() {
809         if (_changedSize) updateCumulativeSizes();
810         int maxscrollx = _fullScrollableArea.width - _visibleScrollableArea.width;
811         int maxscrolly = _fullScrollableArea.height - _visibleScrollableArea.height;
812         if (_scrollX < 0)
813             _scrollX = 0;
814         if (_scrollY < 0)
815             _scrollY = 0;
816         if (_scrollX > maxscrollx)
817             _scrollX = maxscrollx;
818         if (_scrollY > maxscrolly)
819             _scrollY = maxscrolly;
820     }
821 
822     /// set scroll position to show specified cell as top left in scrollable area; col or row -1 value means no change
823     bool scrollTo(int x, int y, GridWidgetBase source = null, bool doNotify = true) {
824         if (_changedSize) updateCumulativeSizes();
825         int oldx = _scrollX;
826         int oldy = _scrollY;
827         _scrollX = x;
828         _scrollY = y;
829         correctScrollPos();
830         updateScrollBars();
831         invalidate();
832         bool changed = oldx != _scrollX || oldy != _scrollY;
833         if (doNotify && changed && viewScrolled.assigned) {
834             if (source is null)
835                 source = this;
836             viewScrolled(source, x, y);
837         }
838         return changed;
839     }
840 
841     /// process horizontal scrollbar event
842     override bool onHScroll(ScrollEvent event) {
843         if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
844             scrollTo(event.position, _scrollY);
845         } else if (event.action == ScrollAction.PageUp) {
846             dispatchAction(new Action(GridActions.ScrollPageLeft));
847         } else if (event.action == ScrollAction.PageDown) {
848             dispatchAction(new Action(GridActions.ScrollPageRight));
849         } else if (event.action == ScrollAction.LineUp) {
850             dispatchAction(new Action(GridActions.ScrollLeft));
851         } else if (event.action == ScrollAction.LineDown) {
852             dispatchAction(new Action(GridActions.ScrollRight));
853         }
854         return true;
855     }
856 
857     /// process vertical scrollbar event
858     override bool onVScroll(ScrollEvent event) {
859         if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
860             scrollTo(_scrollX, event.position);
861         } else if (event.action == ScrollAction.PageUp) {
862             dispatchAction(new Action(GridActions.ScrollPageUp));
863         } else if (event.action == ScrollAction.PageDown) {
864             dispatchAction(new Action(GridActions.ScrollPageDown));
865         } else if (event.action == ScrollAction.LineUp) {
866             dispatchAction(new Action(GridActions.ScrollUp));
867         } else if (event.action == ScrollAction.LineDown) {
868             dispatchAction(new Action(GridActions.ScrollDown));
869         }
870         return true;
871     }
872 
873     /// ensure that cell is visible (scroll if necessary)
874     void makeCellVisible(int col, int row) {
875         if (_changedSize) updateCumulativeSizes();
876         bool scrolled = false;
877         int newx = _scrollX;
878         int newy = _scrollY;
879         Rect rc = cellRectNoScroll(col, row);
880         Rect visibleRc = _visibleScrollableArea;
881         if (col >= nonScrollCols) {
882             // can scroll X
883             if (rc.left < visibleRc.left) {
884                 // scroll left
885                 newx += rc.left - visibleRc.left;
886             } else if (rc.right > visibleRc.right) {
887                 // scroll right
888                 newx += rc.right - visibleRc.right;
889             }
890         }
891         if (row >= nonScrollRows) {
892             // can scroll Y
893             if (rc.top < visibleRc.top) {
894                 // scroll left
895                 newy += rc.top - visibleRc.top;
896             } else if (rc.bottom > visibleRc.bottom) {
897                 // scroll right
898                 newy += rc.bottom - visibleRc.bottom;
899             }
900         }
901         if (newy < 0)
902             newy = 0;
903         if (newx < 0)
904             newx = 0;
905         if (newx != _scrollX || newy != _scrollY) {
906             scrollTo(newx, newy);
907         }
908     }
909 
910 	bool multiSelectCell(int col, int row, bool expandExisting = false) {
911         if (_col == col && _row == row && !expandExisting)
912             return false; // same position
913         if (col < _headerCols || row < _headerRows || col >= _cols || row >= _rows)
914             return false; // out of range
915         if (_changedSize) updateCumulativeSizes();
916         _lastSelectedCell.x = col;
917         _lastSelectedCell.y = row;
918         if (_rowSelect) col = _headerCols;
919         if (expandExisting) {
920             _selection.clear();
921             int startX = _col - _headerCols;
922             int startY = _row - headerRows;
923             int endX = col - _headerCols;
924             int endY = row - headerRows;
925             if (_rowSelect) startX = 0;
926             if (startX > endX) {
927                 startX = endX;
928                 endX = _col - _headerCols;
929             }
930             if (startY > endY) {
931                 startY = endY;
932                 endY = _row - _headerRows;
933             }
934             for (int x = startX; x <= endX; ++x) {
935                 for (int y = startY; y <= endY; ++y) {
936                     _selection.insert(Point(x, y));
937                 }
938             }
939         } else {
940             _selection.insert(Point(col - _headerCols, row - _headerRows));
941             _col = col;
942             _row = row;
943         }
944         invalidate();
945         calcScrollableAreaPos();
946         makeCellVisible(_lastSelectedCell.x, _lastSelectedCell.y);
947         return true;
948 	}
949 
950     /// move selection to specified cell
951     bool selectCell(int col, int row, bool makeVisible = true, GridWidgetBase source = null, bool needNotification = true) {
952         if (source is null)
953             source = this;
954         _selection.clear();
955         if (_col == col && _row == row)
956             return false; // same position
957         if (col < _headerCols || row < _headerRows || col >= _cols || row >= _rows)
958             return false; // out of range
959         if (_changedSize) updateCumulativeSizes();
960         _col = col;
961         _row = row;
962         _lastSelectedCell = Point(col, row);
963         if (_rowSelect) {
964             _selection.insert(Point(0, row - _headerRows));
965         } else {
966             _selection.insert(Point(col - _headerCols, row - _headerRows));
967         }
968         invalidate();
969         calcScrollableAreaPos();
970         if (makeVisible)
971             makeCellVisible(_col, _row);
972         if (needNotification && cellSelected.assigned)
973             cellSelected(source, _col - _headerCols, _row - _headerRows);
974         return true;
975     }
976 
977     /// Select cell and call onCellActivated handler
978     bool activateCell(int col, int row) {
979         if (_changedSize) updateCumulativeSizes();
980         if (_col != col || _row != row) {
981             selectCell(col, row, true);
982         }
983         if (cellActivated.assigned)
984             cellActivated(this, this.col, this.row);
985         return true;
986     }
987 
988     /// cell popup menu
989     Signal!CellPopupMenuHandler cellPopupMenu;
990     /// popup menu item action
991     Signal!MenuItemActionHandler menuItemAction;
992 
993     protected MenuItem getCellPopupMenu(int col, int row) {
994         if (cellPopupMenu.assigned)
995             return cellPopupMenu(this, col, row);
996         return null;
997     }
998 
999     /// handle popup menu action
1000     protected bool onMenuItemAction(const Action action) {
1001         if (menuItemAction.assigned)
1002             return menuItemAction(action);
1003         return false;
1004     }
1005 
1006     /// returns true if widget can show popup menu (e.g. by mouse right click at point x,y)
1007     override bool canShowPopupMenu(int x, int y) {
1008         int col, row;
1009         Rect rc;
1010         x -= _clientRect.left;
1011         y -= _clientRect.top;
1012         pointToCell(x, y, col, row, rc);
1013         MenuItem item = getCellPopupMenu(col - _headerCols, row - _headerRows);
1014         if (!item)
1015             return false;
1016         return true;
1017     }
1018 
1019     /// shows popup menu at (x,y)
1020     override void showPopupMenu(int xx, int yy) {
1021         int col, row;
1022         Rect rc;
1023         int x = xx - _clientRect.left;
1024         int y = yy - _clientRect.top;
1025         pointToCell(x, y, col, row, rc);
1026         MenuItem menu = getCellPopupMenu(col - _headerCols, row - _headerRows);
1027         if (menu) {
1028             import dlangui.widgets.popup;
1029             menu.updateActionState(this);
1030             PopupMenu popupMenu = new PopupMenu(menu);
1031             popupMenu.menuItemAction = this;
1032             PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, xx, yy);
1033             popup.flags = PopupFlags.CloseOnClickOutside;
1034         }
1035     }
1036 
1037     /// returns mouse cursor type for widget
1038     override uint getCursorType(int x, int y) {
1039         if (_allowColResizing) {
1040             if (_colResizingIndex >= 0) // resizing in progress
1041                 return CursorType.SizeWE;
1042             int col = isColumnResizingPoint(x, y);
1043             if (col >= 0)
1044                 return CursorType.SizeWE;
1045         }
1046         return CursorType.Arrow;
1047     }
1048 
1049     protected int _colResizingIndex = -1;
1050     protected int _colResizingStartX = -1;
1051     protected int _colResizingStartWidth = -1;
1052 
1053     protected void startColResize(int col, int x) {
1054         _colResizingIndex = col;
1055         _colResizingStartX = x;
1056         _colResizingStartWidth = _colWidths[col];
1057     }
1058     protected void processColResize(int x) {
1059         if (_colResizingIndex < 0 || _colResizingIndex >= _cols)
1060             return;
1061         int newWidth = _colResizingStartWidth + x - _colResizingStartX;
1062         if (newWidth < 0)
1063             newWidth = 0;
1064         _colWidths[_colResizingIndex] = newWidth;
1065         _changedSize = true;
1066         updateCumulativeSizes();
1067         updateScrollBars();
1068         invalidate();
1069     }
1070     protected void endColResize() {
1071         _colResizingIndex = -1;
1072     }
1073 
1074     /// return column index to resize if point is in column resize area in header row, -1 if outside resize area
1075     int isColumnResizingPoint(int x, int y) {
1076         if (_changedSize) updateCumulativeSizes();
1077         x -= _clientRect.left;
1078         y -= _clientRect.top;
1079         if (!_headerRows)
1080             return -1; // no header rows
1081         if (y >= _rowCumulativeHeights[_headerRows - 1])
1082             return -1; // not in header row
1083         // point is somewhere in header row
1084         int resizeRange = BACKEND_GUI ? 4.pointsToPixels : 1;
1085         if (x >= nonScrollAreaPixels.x)
1086             x += _scrollX;
1087         int col = colByAbsoluteX(x);
1088         int start = col > 0 ? _colCumulativeWidths[col - 1] : 0;
1089         int end = (col < _cols ? _colCumulativeWidths[col] : _colCumulativeWidths[$ - 1]) - 1;
1090         //Log.d("column range ", start, "..", end, " x=", x);
1091         if (x >= end - resizeRange / 2)
1092             return col; // resize this column
1093         if (x <= start + resizeRange / 2)
1094             return col - 1; // resize previous column
1095         return -1;
1096     }
1097 
1098     /// handle mouse wheel events
1099     override bool onMouseEvent(MouseEvent event) {
1100         if (visibility != Visibility.Visible)
1101             return false;
1102         int c, r; // col, row
1103         Rect rc;
1104         bool cellFound = false;
1105         bool normalCell = false;
1106         bool insideHeaderRow = false;
1107         bool insideHeaderCol = false;
1108         if (_colResizingIndex >= 0) { 
1109             if (event.action == MouseAction.Move) {
1110                 // column resize is active
1111                 processColResize(event.x);
1112                 return true;
1113             }
1114             if (event.action == MouseAction.ButtonUp || event.action == MouseAction.Cancel) {
1115                 // stop column resizing
1116                 if (event.action == MouseAction.ButtonUp)
1117                     processColResize(event.x);
1118                 endColResize();
1119                 return true;
1120             }
1121         }
1122         // convert coordinates
1123         if (event.action == MouseAction.ButtonUp || event.action == MouseAction.ButtonDown || event.action == MouseAction.Move) {
1124             int x = event.x;
1125             int y = event.y;
1126             x -= _clientRect.left;
1127             y -= _clientRect.top;
1128             if (_headerRows)
1129                 insideHeaderRow = y < _rowCumulativeHeights[_headerRows - 1];
1130             if (_headerCols)
1131                 insideHeaderCol = y < _colCumulativeWidths[_headerCols - 1];
1132             cellFound = pointToCell(x, y, c, r, rc);
1133             normalCell = c >= _headerCols && r >= _headerRows;
1134         }
1135         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
1136             if (canFocus && !focused)
1137                 setFocus();
1138             int resizeCol = isColumnResizingPoint(event.x, event.y);
1139             if (resizeCol >= 0) {
1140                 // start column resizing
1141                 startColResize(resizeCol, event.x);
1142                 event.track(this);
1143                 return true;
1144             }
1145             if (cellFound && normalCell) {
1146                 if (c == _col && r == _row && event.doubleClick) {
1147                     activateCell(c, r);
1148                 } else if (_multiSelect && (event.flags & (MouseFlag.Shift | MouseFlag.Control)) != 0) {
1149                     multiSelectCell(c, r, (event.flags & MouseFlag.Shift) != 0);
1150                 } else {
1151                     selectCell(c, r);
1152                 }
1153             }
1154             return true;
1155         }
1156         if (event.action == MouseAction.Move && (event.flags & MouseFlag.LButton)) {
1157             // TODO: selection
1158             if (cellFound && normalCell) {
1159                 if (_multiSelect) {
1160                     multiSelectCell(c, r, true);
1161                 } else {
1162                     selectCell(c, r);
1163                 }
1164             }
1165             return true;
1166         }
1167         if (event.action == MouseAction.Wheel) {
1168             if (event.flags & MouseFlag.Shift)
1169                 scrollBy(-event.wheelDelta, 0);
1170             else
1171                 scrollBy(0, -event.wheelDelta);
1172             return true;
1173         }
1174         return super.onMouseEvent(event);
1175     }
1176 
1177 
1178     /// calculate scrollable area info
1179     protected void calcScrollableAreaPos() {
1180         if (_scrollX < 0)
1181             _scrollX = 0;
1182         if (_scrollY < 0)
1183             _scrollY = 0;
1184         // calculate _fullScrollableArea, _visibleScrollableArea relative to clientRect
1185         Point nonscrollPixels = nonScrollAreaPixels;
1186         Point scrollPixels = scrollAreaPixels;
1187         Point fullPixels = fullAreaPixels;
1188         Point clientPixels = _clientRect.size;
1189         Point scrollableClient = clientPixels - nonscrollPixels;
1190         if (scrollableClient.x < 0)
1191             scrollableClient.x = 0;
1192         if (scrollableClient.y < 0)
1193             scrollableClient.y = 0;
1194         _fullScrollableArea = Rect(nonscrollPixels.x, nonscrollPixels.y, fullPixels.x, fullPixels.y);
1195         if (_fullScrollableArea.right < clientPixels.x)
1196             _fullScrollableArea.right = clientPixels.x;
1197         if (_fullScrollableArea.bottom < clientPixels.y)
1198             _fullScrollableArea.bottom = clientPixels.y;
1199 
1200         // extending scroll area if necessary
1201         int maxscrollx = _fullScrollableArea.right - scrollableClient.x;
1202         int col = colByAbsoluteX(maxscrollx);
1203         int maxscrolly = _fullScrollableArea.bottom - scrollableClient.y;
1204         int row = rowByAbsoluteY(maxscrolly);
1205         Rect rc = cellRectNoScroll(col, row);
1206 
1207         // extend scroll area to show full column at left when scrolled to rightmost column
1208         if (maxscrollx >= nonscrollPixels.x && rc.left < maxscrollx) {
1209             _fullScrollableArea.right += rc.right - maxscrollx;
1210         }
1211 
1212         // extend scroll area to show full row at top when scrolled to end row
1213         if (maxscrolly >= nonscrollPixels.y && rc.top < maxscrolly) {
1214             _fullScrollableArea.bottom += rc.bottom - maxscrolly;
1215         }
1216 
1217         // scrollable area
1218         Point scrollableClientAreaSize = scrollableClient; // size left for scrollable area
1219         int scrollx = nonscrollPixels.x + _scrollX;
1220         int scrolly = nonscrollPixels.y + _scrollY;
1221         _visibleScrollableArea = Rect(scrollx, scrolly, scrollx + scrollableClientAreaSize.x, scrolly + scrollableClientAreaSize.y);
1222     }
1223 
1224     protected int _maxScrollCol;
1225     protected int _maxScrollRow;
1226 
1227     override protected bool handleAction(const Action a) {
1228         calcScrollableAreaPos();
1229         int actionId = a.id;
1230         if (_rowSelect) {
1231             switch(actionId) with(GridActions)
1232             {
1233                 case Left:
1234                     actionId = GridActions.ScrollLeft;
1235                     break;
1236                 case Right:
1237                     actionId = GridActions.ScrollRight;
1238                     break;
1239                 //case LineBegin:
1240                 //    actionId = GridActions.ScrollPageLeft;
1241                 //    break;
1242                 //case LineEnd:
1243                 //    actionId = GridActions.ScrollPageRight;
1244                 //    break;
1245                 default:
1246                     break;
1247             }
1248         }
1249 
1250         int sc = scrollCol; // first fully visible column in scroll area
1251         int sr = scrollRow; // first fully visible row in scroll area
1252         switch (actionId) with(GridActions)
1253         {
1254             case ActivateCell:
1255                 if (cellActivated.assigned) {
1256                     cellActivated(this, col, row);
1257                     return true;
1258                 }
1259                 return false;
1260             case ScrollLeft:
1261                 scrollBy(-1, 0);
1262                 return true;
1263             case Left:
1264                 selectCell(_col - 1, _row);
1265                 return true;
1266             case SelectLeft:
1267                 if (_multiSelect) {
1268                     multiSelectCell(_lastSelectedCell.x - 1, _lastSelectedCell.y, true);
1269                 } else {
1270                     selectCell(_col - 1, _row);
1271                 }
1272                 return true;
1273             case ScrollRight:
1274                 scrollBy(1, 0);
1275                 return true;
1276             case Right:
1277                 selectCell(_col + 1, _row);
1278                 return true;
1279             case SelectRight:
1280                 if (_multiSelect) {
1281                     multiSelectCell(_lastSelectedCell.x + 1, _lastSelectedCell.y, true);
1282                 } else {
1283                     selectCell(_col + 1, _row);
1284                 }
1285                 return true;
1286             case ScrollUp:
1287                 scrollBy(0, -1);
1288                 return true;
1289             case Up:
1290                 selectCell(_col, _row - 1);
1291                 return true;
1292             case SelectUp:
1293                 if (_multiSelect) {
1294                     multiSelectCell(_lastSelectedCell.x, _lastSelectedCell.y - 1, true);
1295                 } else {
1296                     selectCell(_col, _row - 1);
1297                 }
1298                 return true;
1299             case ScrollDown:
1300                 if (lastScrollRow < _rows - 1)
1301                     scrollBy(0, 1);
1302                 return true;
1303             case Down:
1304                 selectCell(_col, _row + 1);
1305                 return true;
1306             case SelectDown:
1307                 if (_multiSelect) {
1308                     multiSelectCell(_lastSelectedCell.x, _lastSelectedCell.y + 1, true);
1309                 } else {
1310                     selectCell(_col, _row + 1);
1311                 }
1312                 return true;
1313             case ScrollPageLeft:
1314                 // scroll left cell by cell
1315                 while (scrollCol > nonScrollCols) {
1316                     scrollBy(-1, 0);
1317                     if (lastScrollCol <= sc)
1318                         break;
1319                 }
1320                 return true;
1321             case ScrollPageRight:
1322                 int prevCol = lastScrollCol;
1323                 while (scrollCol < prevCol) {
1324                     if (!scrollBy(1, 0))
1325                         break;
1326                 }
1327                 return true;
1328             case ScrollPageUp:
1329                 // scroll up line by line
1330                 while (scrollRow > nonScrollRows) {
1331                     scrollBy(0, -1);
1332                     if (lastScrollRow <= sr)
1333                         break;
1334                 }
1335                 return true;
1336             case ScrollPageDown:
1337                 int prevRow = lastScrollRow;
1338                 while (scrollRow < prevRow) {
1339                     if (!scrollBy(0, 1))
1340                         break;
1341                 }
1342                 return true;
1343             case SelectLineBegin:
1344                 if (!_multiSelect) goto case LineBegin;
1345                 if (_rowSelect) goto case SelectDocumentBegin;
1346                 if (sc > nonScrollCols && _col > sc) {
1347                     multiSelectCell(sc, _lastSelectedCell.y, true);
1348                 } else {
1349                     if (sc > nonScrollCols) {
1350                         _scrollX = 0;
1351                         updateScrollBars();
1352                         invalidate();
1353                     }
1354                     multiSelectCell(_headerCols, _lastSelectedCell.y, true);
1355                 }
1356                 return true;
1357             case LineBegin:
1358                 if (_rowSelect) goto case DocumentBegin;
1359                 if (sc > nonScrollCols && _col > sc) {
1360                     // move selection and don's scroll
1361                     selectCell(sc, _row);
1362                 } else {
1363                     // scroll
1364                     if (sc > nonScrollCols) {
1365                         _scrollX = 0;
1366                         updateScrollBars();
1367                         invalidate();
1368                     }
1369                     selectCell(_headerCols, _row);
1370                 }
1371                 return true;
1372             case SelectLineEnd:
1373                 if (!_multiSelect) goto case LineEnd;
1374                 if (_rowSelect) goto case SelectDocumentEnd;
1375                 if (_col < lastScrollCol) {
1376                     // move selection and don's scroll
1377                     multiSelectCell(lastScrollCol, _lastSelectedCell.y, true);
1378                 } else {
1379                     multiSelectCell(_cols - 1, _lastSelectedCell.y, true);
1380                 }
1381                 return true;
1382             case LineEnd:
1383                 if (_rowSelect) goto case DocumentEnd;
1384                 if (_col < lastScrollCol) {
1385                     // move selection and don's scroll
1386                     selectCell(lastScrollCol, _row);
1387                 } else {
1388                     selectCell(_cols - 1, _row);
1389                 }
1390                 return true;
1391             case SelectDocumentBegin:
1392                 if (!_multiSelect) goto case DocumentBegin;
1393                 if (_scrollY > 0) {
1394                     _scrollY = 0;
1395                     updateScrollBars();
1396                     invalidate();
1397                 }
1398                 multiSelectCell(_lastSelectedCell.x, _headerRows, true);
1399                 return true;
1400             case DocumentBegin:
1401                 if (_scrollY > 0) {
1402                     _scrollY = 0;
1403                     updateScrollBars();
1404                     invalidate();
1405                 }
1406                 selectCell(_col, _headerRows);
1407                 return true;
1408             case SelectDocumentEnd:
1409                 if (!_multiSelect) goto case DocumentEnd;
1410                 multiSelectCell(_lastSelectedCell.x, _rows - 1, true);
1411                 return true;
1412             case DocumentEnd:
1413                 selectCell(_col, _rows - 1);
1414                 return true;
1415             case SelectAll:
1416                 if (!_multiSelect) return true;
1417                 int endX = row;
1418                 if (_rowSelect) endX = 0;
1419                 for (int x = 0; x <= endX; ++x) {
1420                     for (int y = 0; y < rows; ++y) {
1421                         _selection.insert(Point(x, y));
1422                     }
1423                 }
1424                 invalidate();
1425                 return true;
1426             case SelectPageBegin:
1427                 if (!_multiSelect) goto case PageBegin;
1428                 if (scrollRow > nonScrollRows)
1429                     multiSelectCell(_lastSelectedCell.x, scrollRow, true);
1430                 else
1431                     multiSelectCell(_lastSelectedCell.x, _headerRows, true);
1432                 return true;
1433             case PageBegin:
1434                 if (scrollRow > nonScrollRows)
1435                     selectCell(_col, scrollRow);
1436                 else
1437                     selectCell(_col, _headerRows);
1438                 return true;
1439             case SelectPageEnd:
1440                 if (!_multiSelect) goto case PageEnd;
1441                 multiSelectCell(_lastSelectedCell.x, lastScrollRow, true);
1442                 return true;
1443             case PageEnd:
1444                 selectCell(_col, lastScrollRow);
1445                 return true;
1446             case SelectPageUp:
1447                 if (_row > sr) {
1448                     // not at top scrollable cell
1449                     multiSelectCell(_lastSelectedCell.x, sr, true);
1450                 } else {
1451                     // at top of scrollable area
1452                     if (scrollRow > nonScrollRows) {
1453                         // scroll up line by line
1454                         int prevRow = _row;
1455                         for (int i = prevRow - 1; i >= _headerRows; i--) {
1456                             multiSelectCell(_lastSelectedCell.x, i, true);
1457                             if (lastScrollRow <= prevRow)
1458                                 break;
1459                         }
1460                     } else {
1461                         // scrolled to top - move upper cell
1462                         multiSelectCell(_lastSelectedCell.x, _headerRows, true);
1463                     }
1464                 }
1465                 return true;
1466             case PageUp:
1467                 if (_row > sr) {
1468                     // not at top scrollable cell
1469                     selectCell(_col, sr);
1470                 } else {
1471                     // at top of scrollable area
1472                     if (scrollRow > nonScrollRows) {
1473                         // scroll up line by line
1474                         int prevRow = _row;
1475                         for (int i = prevRow - 1; i >= _headerRows; i--) {
1476                             selectCell(_col, i);
1477                             if (lastScrollRow <= prevRow)
1478                                 break;
1479                         }
1480                     } else {
1481                         // scrolled to top - move upper cell
1482                         selectCell(_col, _headerRows);
1483                     }
1484                 }
1485                 return true;
1486             case SelectPageDown:
1487                 if (_row < _rows - 1) {
1488                     int lr = lastScrollRow;
1489                     if (_row < lr) {
1490                         // not at bottom scrollable cell
1491                         multiSelectCell(_lastSelectedCell.x, lr, true);
1492                     } else {
1493                         // scroll down
1494                         int prevRow = _row;
1495                         for (int i = prevRow + 1; i < _rows; i++) {
1496                             multiSelectCell(_lastSelectedCell.x, i, true);
1497                             calcScrollableAreaPos();
1498                             if (scrollRow >= prevRow)
1499                                 break;
1500                         }
1501                     }
1502                 }
1503                 return true;
1504             case PageDown:
1505                 if (_row < _rows - 1) {
1506                     int lr = lastScrollRow;
1507                     if (_row < lr) {
1508                         // not at bottom scrollable cell
1509                         selectCell(_col, lr);
1510                     } else {
1511                         // scroll down
1512                         int prevRow = _row;
1513                         for (int i = prevRow + 1; i < _rows; i++) {
1514                             selectCell(_col, i);
1515                             calcScrollableAreaPos();
1516                             if (scrollRow >= prevRow)
1517                                 break;
1518                         }
1519                     }
1520                 }
1521                 return true;
1522             default:
1523                 return super.handleAction(a);
1524         }
1525     }
1526 
1527     /// calculate full content size in pixels
1528     override Point fullContentSize() {
1529         Point sz;
1530         for (int i = 0; i < _cols; i++)
1531             sz.x += _colWidths[i];
1532         for (int i = 0; i < _rows; i++)
1533             sz.y += _rowHeights[i];
1534         return sz;
1535     }
1536 
1537     override protected void drawClient(DrawBuf buf) {
1538         if (!_cols || !_rows)
1539             return; // no cells
1540         auto saver = ClipRectSaver(buf, _clientRect, 0);
1541 
1542         int nscols = nonScrollCols;
1543         int nsrows = nonScrollRows;
1544         Point nspixels = nonScrollAreaPixels;
1545         int maxVisibleCol = colByAbsoluteX(_clientRect.width + _scrollX);
1546         int maxVisibleRow = rowByAbsoluteY(_clientRect.height + _scrollY);
1547         for (int phase = 0; phase < 2; phase++) { // phase0 == background, phase1 == foreground
1548             for (int y = 0; y <= maxVisibleRow; y++) {
1549                 if (!rowVisible(y))
1550                     continue;
1551                 for (int x = 0; x <= maxVisibleCol; x++) {
1552                     if (!colVisible(x))
1553                         continue;
1554                     Rect cellRect = cellRectScroll(x, y);
1555                     if (BACKEND_CONSOLE && phase == 1) {
1556                         cellRect.right--;
1557                     }
1558                     Rect clippedCellRect = cellRect;
1559                     if (x >= nscols && cellRect.left < nspixels.x)
1560                         clippedCellRect.left = nspixels.x; // clip scrolled left
1561                     if (y >= nsrows && cellRect.top < nspixels.y)
1562                         clippedCellRect.top = nspixels.y; // clip scrolled left
1563                     if (clippedCellRect.empty)
1564                         continue; // completely clipped out
1565 
1566                     cellRect.moveBy(_clientRect.left, _clientRect.top);
1567                     clippedCellRect.moveBy(_clientRect.left, _clientRect.top);
1568 
1569                     auto cellSaver = ClipRectSaver(buf, clippedCellRect, 0);
1570                     bool isHeader = x < _headerCols || y < _headerRows;
1571                     if (phase == 0) {
1572                         if (isHeader)
1573                             drawHeaderCellBackground(buf, cellRect, x - _headerCols, y - _headerRows);
1574                         else
1575                             drawCellBackground(buf, cellRect, x - _headerCols, y - _headerRows);
1576                     } else {
1577                         if (isHeader)
1578                             drawHeaderCell(buf, cellRect, x - _headerCols, y - _headerRows);
1579                         else
1580                             drawCell(buf, cellRect, x - _headerCols, y - _headerRows);
1581                     }
1582                 }
1583             }
1584         }
1585     }
1586 
1587     /// draw data cell content
1588     protected void drawCell(DrawBuf buf, Rect rc, int col, int row) {
1589         // override it
1590     }
1591 
1592     /// draw header cell content
1593     protected void drawHeaderCell(DrawBuf buf, Rect rc, int col, int row) {
1594         // override it
1595     }
1596 
1597     /// draw data cell background
1598     protected void drawCellBackground(DrawBuf buf, Rect rc, int col, int row) {
1599         // override it
1600     }
1601 
1602     /// draw header cell background
1603     protected void drawHeaderCellBackground(DrawBuf buf, Rect rc, int col, int row) {
1604         // override it
1605     }
1606 
1607     protected Point measureCell(int x, int y) {
1608         // override it!
1609         return Point(BACKEND_CONSOLE ? 5 : 80, BACKEND_CONSOLE ? 1 : 20);
1610     }
1611 
1612     protected int measureColWidth(int x) {
1613         int m = 0;
1614         for (int i = 0; i < _rows; i++) {
1615             Point sz = measureCell(x - _headerCols, i - _headerRows);
1616             if (m < sz.x)
1617                 m = sz.x;
1618         }
1619         //Log.d("measureColWidth ", x, " = ", m);
1620         static if (BACKEND_GUI) {
1621             if (m < 10)
1622                 m = 10; // TODO: use min size
1623         } else {
1624             if (m < 1)
1625                 m = 1; // TODO: use min size
1626         }
1627         return m;
1628     }
1629 
1630     protected int measureRowHeight(int y) {
1631         int m = 0;
1632         for (int i = 0; i < _cols; i++) {
1633             Point sz = measureCell(i - _headerCols, y - _headerRows);
1634             if (m < sz.y)
1635                 m = sz.y;
1636         }
1637         static if (BACKEND_GUI) {
1638             if (m < 12)
1639                 m = 12; // TODO: use min size
1640         }
1641         return m;
1642     }
1643 
1644     void autoFitColumnWidth(int i) {
1645         _colWidths[i] = (i < _headerCols && !_showRowHeaders) ? 0 : measureColWidth(i) + (BACKEND_CONSOLE ? 1 : 3.pointsToPixels);
1646         _changedSize = true;
1647     }
1648 
1649     /// extend specified column width to fit client area if grid width
1650     void fillColumnWidth(int colIndex) {
1651         int w = _clientRect.width;
1652         int totalw = 0;
1653         for (int i = 0; i < _cols; i++)
1654             totalw += _colWidths[i];
1655         if (w > totalw)
1656             _colWidths[colIndex + _headerCols] += w - totalw;
1657         _changedSize = true;
1658         invalidate();
1659     }
1660 
1661     void autoFitColumnWidths() {
1662         for (int i = 0; i < _cols; i++)
1663             autoFitColumnWidth(i);
1664         _changedSize = true;
1665         invalidate();
1666     }
1667 
1668     void autoFitRowHeight(int i) {
1669         _rowHeights[i] = (i < _headerRows && !_showColHeaders) ? 0 : measureRowHeight(i) + (BACKEND_CONSOLE ? 0 : 2);
1670         _changedSize = true;
1671     }
1672 
1673     void autoFitRowHeights() {
1674         for (int i = 0; i < _rows; i++)
1675             autoFitRowHeight(i);
1676     }
1677 
1678     void autoFit() {
1679         autoFitColumnWidths();
1680         autoFitRowHeights();
1681         updateCumulativeSizes();
1682     }
1683 
1684     this(string ID = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
1685         super(ID, hscrollbarMode, vscrollbarMode);
1686         _headerCols = 1;
1687         _headerRows = 1;
1688         _selection = new RedBlackTree!Point();
1689         _defRowHeight = BACKEND_CONSOLE ? 1 : pointsToPixels(16);
1690         _defColumnWidth = BACKEND_CONSOLE ? 7 : 100;
1691 
1692         _showColHeaders = true;
1693         _showRowHeaders = true;
1694         acceleratorMap.add( [
1695             new Action(GridActions.Up, KeyCode.UP, 0),
1696             new Action(GridActions.Down, KeyCode.DOWN, 0),
1697             new Action(GridActions.Left, KeyCode.LEFT, 0),
1698             new Action(GridActions.Right, KeyCode.RIGHT, 0),
1699             new Action(GridActions.LineBegin, KeyCode.HOME, 0),
1700             new Action(GridActions.LineEnd, KeyCode.END, 0),
1701             new Action(GridActions.PageUp, KeyCode.PAGEUP, 0),
1702             new Action(GridActions.PageDown, KeyCode.PAGEDOWN, 0),
1703             new Action(GridActions.PageBegin, KeyCode.PAGEUP, KeyFlag.Control),
1704             new Action(GridActions.PageEnd, KeyCode.PAGEDOWN, KeyFlag.Control),
1705             new Action(GridActions.DocumentBegin, KeyCode.HOME, KeyFlag.Control),
1706             new Action(GridActions.DocumentEnd, KeyCode.END, KeyFlag.Control),
1707             new Action(GridActions.SelectUp, KeyCode.UP, KeyFlag.Shift),
1708             new Action(GridActions.SelectDown, KeyCode.DOWN, KeyFlag.Shift),
1709             new Action(GridActions.SelectLeft, KeyCode.LEFT, KeyFlag.Shift),
1710             new Action(GridActions.SelectRight, KeyCode.RIGHT, KeyFlag.Shift),
1711             new Action(GridActions.SelectLineBegin, KeyCode.HOME, KeyFlag.Shift),
1712             new Action(GridActions.SelectLineEnd, KeyCode.END, KeyFlag.Shift),
1713             new Action(GridActions.SelectPageUp, KeyCode.PAGEUP, KeyFlag.Shift),
1714             new Action(GridActions.SelectPageDown, KeyCode.PAGEDOWN, KeyFlag.Shift),
1715             new Action(GridActions.SelectPageBegin, KeyCode.PAGEUP, KeyFlag.Control | KeyFlag.Shift),
1716             new Action(GridActions.SelectPageEnd, KeyCode.PAGEDOWN, KeyFlag.Control | KeyFlag.Shift),
1717             new Action(GridActions.SelectDocumentBegin, KeyCode.HOME, KeyFlag.Control | KeyFlag.Shift),
1718             new Action(GridActions.SelectDocumentEnd, KeyCode.END, KeyFlag.Control | KeyFlag.Shift),
1719             new Action(GridActions.SelectAll, KeyCode.KEY_A, KeyFlag.Control),
1720             new Action(GridActions.ActivateCell, KeyCode.RETURN, 0),
1721         ]);
1722         focusable = true;
1723     }
1724 }
1725 
1726 class StringGridWidgetBase : GridWidgetBase {
1727     this(string ID = null) {
1728         super(ID);
1729     }
1730     /// get cell text
1731     abstract dstring cellText(int col, int row);
1732     /// set cell text
1733     abstract StringGridWidgetBase setCellText(int col, int row, dstring text);
1734     /// returns row header title
1735     abstract dstring rowTitle(int row);
1736     /// set row header title
1737     abstract StringGridWidgetBase setRowTitle(int row, dstring title);
1738     /// returns row header title
1739     abstract dstring colTitle(int col);
1740     /// set col header title
1741     abstract StringGridWidgetBase setColTitle(int col, dstring title);
1742 
1743     ///// selected column
1744     //@property override int col() { return _col - _headerCols; }
1745     ///// selected row
1746     //@property override int row() { return _row - _headerRows; }
1747     ///// column count
1748     //@property override int cols() { return _cols - _headerCols; }
1749     ///// set column count
1750     //@property override GridWidgetBase cols(int c) { resize(c, rows); return this; }
1751     ///// row count
1752     //@property override int rows() { return _rows - _headerRows; }
1753     ///// set row count
1754     //@property override GridWidgetBase rows(int r) { resize(cols, r); return this; }
1755     //
1756     ///// set new size
1757     //override void resize(int cols, int rows) {
1758     //    super.resize(cols + _headerCols, rows + _headerRows);
1759     //}
1760 
1761 }
1762 
1763 /**
1764  * Grid view with string data shown. All rows are of the same height
1765  */
1766 class StringGridWidget : StringGridWidgetBase {
1767 
1768     protected dstring[][] _data;
1769     protected dstring[] _rowTitles;
1770     protected dstring[] _colTitles;
1771 
1772     /// empty parameter list constructor - for usage by factory
1773     this() {
1774         this(null);
1775     }
1776     /// create with ID parameter
1777     this(string ID) {
1778         super(ID);
1779         styleId = STYLE_STRING_GRID;
1780         onThemeChanged();
1781     }
1782 
1783     /// get cell text
1784     override dstring cellText(int col, int row) {
1785         if (col >= 0 && col < cols && row >= 0 && row < rows)
1786             return _data[row][col];
1787         return ""d;
1788     }
1789 
1790     /// set cell text
1791     override StringGridWidgetBase setCellText(int col, int row, dstring text) {
1792         if (col >= 0 && col < cols && row >= 0 && row < rows)
1793             _data[row][col] = text;
1794         return this;
1795     }
1796 
1797     /// set new size
1798     override void resize(int c, int r) {
1799         if (c == cols && r == rows)
1800             return;
1801         int oldcols = cols;
1802         int oldrows = rows;
1803         super.resize(c, r);
1804         _data.length = r;
1805         for (int y = 0; y < r; y++)
1806             _data[y].length = c;
1807         _colTitles.length = c;
1808         _rowTitles.length = r;
1809     }
1810 
1811     /// returns row header title
1812     override dstring rowTitle(int row) {
1813         return _rowTitles[row];
1814     }
1815     /// set row header title
1816     override StringGridWidgetBase setRowTitle(int row, dstring title) {
1817         _rowTitles[row] = title;
1818         return this;
1819     }
1820 
1821     /// returns row header title
1822     override dstring colTitle(int col) {
1823         return _colTitles[col];
1824     }
1825 
1826     /// set col header title
1827     override StringGridWidgetBase setColTitle(int col, dstring title) {
1828         _colTitles[col] = title;
1829         return this;
1830     }
1831 
1832     protected override Point measureCell(int x, int y) {
1833         if (_customCellAdapter && _customCellAdapter.isCustomCell(x, y)) {
1834             return _customCellAdapter.measureCell(x, y);
1835         }
1836         //Log.d("measureCell ", x, ", ", y);
1837         FontRef fnt = font;
1838         dstring txt;
1839         if (x >= 0 && y >= 0)
1840             txt = cellText(x, y);
1841         else if (y < 0 && x >= 0)
1842             txt = colTitle(x);
1843         else if (y >= 0 && x < 0)
1844             txt = rowTitle(y);
1845         Point sz = fnt.textSize(txt);
1846         if (sz.y < fnt.height)
1847             sz.y = fnt.height;
1848         return sz;
1849     }
1850 
1851 
1852     /// draw cell content
1853     protected override void drawCell(DrawBuf buf, Rect rc, int col, int row) {
1854         if (_customCellAdapter && _customCellAdapter.isCustomCell(col, row)) {
1855             return _customCellAdapter.drawCell(buf, rc, col, row);
1856         }
1857         if (BACKEND_GUI) 
1858             rc.shrink(2, 1);
1859         else 
1860             rc.right--;
1861         FontRef fnt = font;
1862         dstring txt = cellText(col, row);
1863         Point sz = fnt.textSize(txt);
1864         Align ha = Align.Left;
1865         //if (sz.y < rc.height)
1866         //    applyAlign(rc, sz, ha, Align.VCenter);
1867         int offset = BACKEND_CONSOLE ? 0 : 1;
1868         fnt.drawText(buf, rc.left + offset, rc.top + offset, txt, textColor);
1869     }
1870 
1871     /// draw cell content
1872     protected override void drawHeaderCell(DrawBuf buf, Rect rc, int col, int row) {
1873         if (BACKEND_GUI) 
1874             rc.shrink(2, 1);
1875         else 
1876             rc.right--;
1877         FontRef fnt = font;
1878         dstring txt;
1879         if (row < 0 && col >= 0)
1880             txt = colTitle(col);
1881         else if (row >= 0 && col < 0)
1882             txt = rowTitle(row);
1883         if (!txt.length)
1884             return;
1885         Point sz = fnt.textSize(txt);
1886         Align ha = Align.Left;
1887         if (col < 0)
1888             ha = Align.Right;
1889         //if (row < 0)
1890         //    ha = Align.HCenter;
1891         applyAlign(rc, sz, ha, Align.VCenter);
1892         int offset = BACKEND_CONSOLE ? 0 : 1;
1893         uint cl = textColor;
1894         cl = style.customColor("grid_cell_text_color_header", cl);
1895         fnt.drawText(buf, rc.left + offset, rc.top + offset, txt, cl);
1896     }
1897 
1898     /// draw cell background
1899     protected override void drawHeaderCellBackground(DrawBuf buf, Rect rc, int c, int r) {
1900         bool selectedCol = (c == col) && !_rowSelect;
1901         bool selectedRow = r == row;
1902         bool selectedCell = selectedCol && selectedRow;
1903         if (_rowSelect && selectedRow)
1904             selectedCell = true;
1905         if (!selectedCell && _multiSelect) {
1906             selectedCell = Point(c, r) in _selection || (_rowSelect && Point(0, r) in _selection);
1907         }
1908         // draw header cell background
1909         DrawableRef dw = c < 0 ? _cellRowHeaderBackgroundDrawable : _cellHeaderBackgroundDrawable;
1910         uint cl = _cellHeaderBackgroundColor;
1911         if (c >= 0 || r >= 0) {
1912             if (c < 0 && selectedRow) {
1913                 cl = _cellHeaderSelectedBackgroundColor;
1914                 dw = _cellRowHeaderSelectedBackgroundDrawable;
1915             } else if (r < 0 && selectedCol) {
1916                 cl = _cellHeaderSelectedBackgroundColor;
1917                 dw = _cellHeaderSelectedBackgroundDrawable;
1918             }
1919         }
1920         if (!dw.isNull)
1921             dw.drawTo(buf, rc);
1922         else
1923             buf.fillRect(rc, cl);
1924         static if (BACKEND_GUI) {
1925             uint borderColor = _cellHeaderBorderColor;
1926             buf.drawLine(Point(rc.right - 1, rc.bottom), Point(rc.right - 1, rc.top), _cellHeaderBorderColor); // vertical
1927             buf.drawLine(Point(rc.left, rc.bottom - 1), Point(rc.right - 1, rc.bottom - 1), _cellHeaderBorderColor); // horizontal
1928         }
1929     }
1930 
1931     /// draw cell background
1932     protected override void drawCellBackground(DrawBuf buf, Rect rc, int c, int r) {
1933         bool selectedCol = c == col;
1934         bool selectedRow = r == row;
1935         bool selectedCell = selectedCol && selectedRow;
1936         if (_rowSelect && selectedRow)
1937             selectedCell = true;
1938         if (!selectedCell && _multiSelect) {
1939             selectedCell = Point(c, r) in _selection || (_rowSelect && Point(0, r) in _selection);
1940         }
1941         uint borderColor = _cellBorderColor;
1942         if (c < fixedCols || r < fixedRows) {
1943             // fixed cell background
1944             buf.fillRect(rc, _fixedCellBackgroundColor);
1945             borderColor = _fixedCellBorderColor;
1946         }
1947         static if (BACKEND_GUI) {
1948             buf.drawLine(Point(rc.left, rc.bottom + 1), Point(rc.left, rc.top), borderColor); // vertical
1949             buf.drawLine(Point(rc.left, rc.bottom - 1), Point(rc.right - 1, rc.bottom - 1), borderColor); // horizontal
1950         }
1951         if (selectedCell) {
1952             static if (BACKEND_GUI) {
1953                 if (_rowSelect)
1954                     buf.drawFrame(rc, _selectionColorRowSelect, Rect(0,1,0,1), _cellBorderColor);
1955                 else
1956                     buf.drawFrame(rc, _selectionColor, Rect(1,1,1,1), _cellBorderColor);
1957             } else {
1958                 if (_rowSelect)
1959                     buf.fillRect(rc, _selectionColorRowSelect);
1960                 else
1961                     buf.fillRect(rc, _selectionColor);
1962             }
1963         }
1964     }
1965 
1966 
1967     /// handle theme change: e.g. reload some themed resources
1968     override void onThemeChanged() {
1969         super.onThemeChanged();
1970         _selectionColor = style.customColor("grid_selection_color", 0x804040FF);
1971         _selectionColorRowSelect = style.customColor("grid_selection_color_row", 0xC0A0B0FF);
1972         _fixedCellBackgroundColor = style.customColor("grid_cell_background_fixed", 0xC0E0E0E0);
1973         _cellBorderColor = style.customColor("grid_cell_border_color", 0xC0C0C0C0);
1974         _fixedCellBorderColor = style.customColor("grid_cell_border_color_fixed", _cellBorderColor);
1975         _cellHeaderBorderColor = style.customColor("grid_cell_border_color_header", 0xC0202020);
1976         _cellHeaderBackgroundColor = style.customColor("grid_cell_background_header", 0xC0909090);
1977         _cellHeaderSelectedBackgroundColor = style.customColor("grid_cell_background_header_selected", 0x80FFC040);
1978         _cellHeaderBackgroundDrawable = style.customDrawable("grid_cell_background_header");
1979         _cellHeaderSelectedBackgroundDrawable = style.customDrawable("grid_cell_background_header_selected");
1980         _cellRowHeaderBackgroundDrawable = style.customDrawable("grid_cell_background_row_header");
1981         _cellRowHeaderSelectedBackgroundDrawable = style.customDrawable("grid_cell_background_row_header_selected");
1982     }
1983 }
1984 
1985 //import dlangui.widgets.metadata;
1986 //mixin(registerWidgets!(StringGridWidget)());