loleaflet/dist/spreadsheet.css                |   27 +-
 loleaflet/src/control/Control.ColumnHeader.js |  265 +++++++++++++++----
 loleaflet/src/control/Control.Header.js       |  347 ++++++++++++++++++++++++--
 loleaflet/src/control/Control.Menubar.js      |    9 
 loleaflet/src/control/Control.RowHeader.js    |  248 +++++++++++++++---
 loleaflet/src/control/Control.Scroll.js       |    5 
 loleaflet/src/dom/DomUtil.js                  |   12 
 loleaflet/src/layer/tile/CalcTileLayer.js     |    4 
 8 files changed, 788 insertions(+), 129 deletions(-)

New commits:
commit 550de386482c075e55fecffca4083eaa065ab076
Author: Marco Cecchetti <marco.cecche...@collabora.com>
Date:   Mon Nov 27 19:37:53 2017 +0100

    calc: outline and groups handling
    
    Change-Id: Ie7dcb9a742344e6b0a8813faebc589167a457261
    Reviewed-on: https://gerrit.libreoffice.org/45360
    Reviewed-by: Marco Cecchetti <mrcek...@gmail.com>
    Tested-by: Marco Cecchetti <mrcek...@gmail.com>

diff --git a/loleaflet/dist/spreadsheet.css b/loleaflet/dist/spreadsheet.css
index 8435b271..e97f6041 100644
--- a/loleaflet/dist/spreadsheet.css
+++ b/loleaflet/dist/spreadsheet.css
@@ -59,7 +59,7 @@
        }
 
 #spreadsheet-row-column-frame {
-        position: absolute;
+       position: absolute;
        left: 0;
        right: 0;
        top: 103px;
@@ -70,7 +70,7 @@
         top: 30px;
 }
 
-.spreadsheet-header-corner {
+#spreadsheet-header-corner-container {
        border: 1px solid darkgrey;
        background-color: lightgrey;
        cursor: pointer;
@@ -84,7 +84,26 @@
        height: 19px;
        }
 
-.spreadsheet-header-columns-container {
+#spreadsheet-header-corner {
+       display: inline-block;
+       white-space: nowrap;
+       width: 100%;
+       height: 100%;
+       border-spacing: 0px !important;
+       position: relative;
+       margin: 0px;
+       padding: 0px;
+       }
+
+.spreadsheet-header-corner-styles {
+       border: 1px solid darkgray;
+       font: 12px/1.5 "Segoe UI", Tahoma, Arial, Helvetica, sans-serif;
+       color: black;
+       background-color: lightgray;
+       cursor: pointer;
+       }
+
+#spreadsheet-header-columns-container {
        border: 1px solid darkgrey;
        background-color: lightgrey;
 
@@ -130,7 +149,7 @@
        cursor: col-resize;
 }
 
-.spreadsheet-header-rows-container {
+#spreadsheet-header-rows-container {
        border: 1px solid darkgrey;
        background-color: lightgrey;
 
diff --git a/loleaflet/src/control/Control.ColumnHeader.js 
b/loleaflet/src/control/Control.ColumnHeader.js
index 264fea4d..ae95f183 100644
--- a/loleaflet/src/control/Control.ColumnHeader.js
+++ b/loleaflet/src/control/Control.ColumnHeader.js
@@ -15,38 +15,44 @@ L.Control.ColumnHeader = L.Control.Header.extend({
 
        _initialize: function () {
                this._initialized = true;
+               this._isColumn = true;
                this._map.on('scrolloffset', this.offsetScrollPosition, this);
                this._map.on('updatescrolloffset', this.setScrollPosition, 
this);
                this._map.on('viewrowcolumnheaders', this.viewRowColumnHeaders, 
this);
                this._map.on('updateselectionheader', this._onUpdateSelection, 
this);
                this._map.on('clearselectionheader', this._onClearSelection, 
this);
                this._map.on('updatecurrentheader', 
this._onUpdateCurrentColumn, this);
+               this._map.on('updatecornerheader', this.drawCornerHeader, this);
                var rowColumnFrame = 
L.DomUtil.get('spreadsheet-row-column-frame');
-               var cornerHeader = L.DomUtil.create('div', 
'spreadsheet-header-corner', rowColumnFrame);
-               L.DomEvent.on(cornerHeader, 'contextmenu', 
L.DomEvent.preventDefault);
-               L.DomEvent.addListener(cornerHeader, 'click', 
this._onCornerHeaderClick, this);
-               this._headersContainer = L.DomUtil.create('div', 
'spreadsheet-header-columns-container', rowColumnFrame);
+               this._headerContainer = L.DomUtil.createWithId('div', 
'spreadsheet-header-columns-container', rowColumnFrame);
 
                this._initHeaderEntryStyles('spreadsheet-header-column');
                
this._initHeaderEntryHoverStyles('spreadsheet-header-column-hover');
                
this._initHeaderEntrySelectedStyles('spreadsheet-header-column-selected');
                
this._initHeaderEntryResizeStyles('spreadsheet-header-column-resize');
 
-               this._headerCanvas = L.DomUtil.create('canvas', 
'spreadsheet-header-columns', this._headersContainer);
-               this._canvasContext = this._headerCanvas.getContext('2d');
-               this._headerCanvas.width = 
parseInt(L.DomUtil.getStyle(this._headersContainer, 'width'));
-               this._headerCanvas.height = 
parseInt(L.DomUtil.getStyle(this._headersContainer, 'height'));
+               this._canvas = L.DomUtil.create('canvas', 
'spreadsheet-header-columns', this._headerContainer);
+               this._canvasContext = this._canvas.getContext('2d');
+               this._setCanvasWidth();
+               this._setCanvasHeight();
+               this._headerHeight = this._canvas.height;
+               L.Control.Header.colHeaderHeight = this._canvas.height;
 
-               L.DomUtil.setStyle(this._headerCanvas, 'cursor', this._cursor);
+               L.DomUtil.setStyle(this._canvas, 'cursor', this._cursor);
 
-               L.DomEvent.on(this._headerCanvas, 'mousemove', 
this._onCanvasMouseMove, this);
-               L.DomEvent.on(this._headerCanvas, 'mouseout', this._onMouseOut, 
this);
-               L.DomEvent.on(this._headerCanvas, 'click', this._onHeaderClick, 
this);
+               L.DomEvent.on(this._canvas, 'mousemove', this._onMouseMove, 
this);
+               L.DomEvent.on(this._canvas, 'mouseout', this._onMouseOut, this);
+               L.DomEvent.on(this._canvas, 'click', this._onClick, this);
+               L.DomEvent.on(this._canvas, 'dblclick', this._onDoubleClick, 
this);
 
-               this._leftmostColumn = 0;
-               this._leftOffset = 0;
+               this._startHeaderIndex = 0;
+               this._startOffset = 0;
                this._position = 0;
 
+               L.DomEvent.on(this._cornerCanvas, 'contextmenu', 
L.DomEvent.preventDefault);
+               L.DomEvent.addListener(this._cornerCanvas, 'click', 
this._onCornerHeaderClick, this);
+
+
                var colHeaderObj = this;
                $.contextMenu({
                        selector: '.spreadsheet-header-columns',
@@ -178,11 +184,13 @@ L.Control.ColumnHeader = L.Control.Header.extend({
        },
 
        _onUpdateCurrentColumn: function (e) {
-               var x = e.x;
+               var x = e.min.x;
+               var w = e.getSize().x;
                if (x !== -1) {
                        x = this._twipsToPixels(x);
+                       w = this._twipsToPixels(w);
                }
-               this.updateCurrent(this._data, x);
+               this.updateCurrent(this._data, x, w);
        },
 
        _updateColumnHeader: function () {
@@ -194,46 +202,133 @@ L.Control.ColumnHeader = L.Control.Header.extend({
                        return;
 
                var ctx = this._canvasContext;
-               var content = this._colIndexToAlpha(entry.index + 
this._leftmostColumn);
-               var start = entry.pos - entry.size - this._leftOffset;
-               var end = entry.pos - this._leftOffset;
-               var width = end - start;
-               var height = this._headerCanvas.height;
+               var content = this._colIndexToAlpha(entry.index + 
this._startHeaderIndex);
+               var startOrt = this._canvas.height - this._headerHeight;
+               var startPar = entry.pos - entry.size - this._startOffset;
+               var endPar = entry.pos - this._startOffset;
+               var width = endPar - startPar;
+               var height = this._headerHeight;
 
                if (isHighlighted !== true && isHighlighted !== false) {
                        isHighlighted = this.isHighlighted(entry.index);
                }
 
-
                if (width <= 0)
                        return;
 
                ctx.save();
-               ctx.translate(this._position + this._leftOffset, 0);
+               ctx.translate(this._position + this._startOffset, 0);
                // background gradient
                var selectionBackgroundGradient = null;
                if (isHighlighted) {
-                       selectionBackgroundGradient = 
ctx.createLinearGradient(start, 0, start, height);
+                       selectionBackgroundGradient = 
ctx.createLinearGradient(startPar, startOrt, startPar, startOrt + height);
                        selectionBackgroundGradient.addColorStop(0, 
this._selectionBackgroundGradient[0]);
                        selectionBackgroundGradient.addColorStop(0.5, 
this._selectionBackgroundGradient[1]);
                        selectionBackgroundGradient.addColorStop(1, 
this._selectionBackgroundGradient[2]);
                }
+
+               // draw header/outline border separator
+               if (this._headerHeight !== this._canvas.height) {
+                       ctx.fillStyle = this._borderColor;
+                       ctx.fillRect(startPar, startOrt - this._borderWidth, 
width, this._borderWidth);
+               }
+
                // clip mask
                ctx.beginPath();
-               ctx.rect(start, 0, width, height);
+               ctx.rect(startPar, startOrt, width, height);
                ctx.clip();
                // draw background
                ctx.fillStyle = isHighlighted ? selectionBackgroundGradient : 
isOver ? this._hoverColor : this._backgroundColor;
-               ctx.fillRect(start, 0, width, height);
+               ctx.fillRect(startPar, startOrt, width, height);
                // draw text content
                ctx.fillStyle = isHighlighted ? this._selectionTextColor : 
this._textColor;
                ctx.font = this._font;
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
-               ctx.fillText(content, end - width / 2, height / 2);
+               ctx.fillText(content, endPar - (width / 2), startOrt + (height 
/ 2));
                // draw row separator
                ctx.fillStyle = this._borderColor;
-               ctx.fillRect(end -1, 0, this._borderWidth, height);
+               ctx.fillRect(endPar -1, startOrt, this._borderWidth, height);
+               ctx.restore();
+       },
+
+       drawGroupControl: function (group) {
+               if (!group)
+                       return;
+
+               var ctx = this._canvasContext;
+               var headSize = this._groupHeadSize;
+               var spacing = this._levelSpacing;
+               var level = group.level;
+
+               var startOrt = spacing + (headSize + spacing) * level;
+               var startPar = group.startPos - this._startOffset;
+               var height = group.endPos - group.startPos;
+
+               ctx.save();
+               ctx.translate(this._position + this._startOffset, 0);
+               // clip mask
+               ctx.beginPath();
+               ctx.rect(startPar, startOrt, height, headSize);
+               ctx.clip();
+               if (!group.hidden) {
+                       //draw tail
+                       ctx.strokeStyle = 'black';
+                       ctx.lineWidth = 1.5;
+                       ctx.beginPath();
+                       ctx.moveTo(startPar + headSize, startOrt + 2);
+                       ctx.lineTo(startPar + height - 1, startOrt + 2);
+                       ctx.lineTo(startPar + height - 1, startOrt + 2 + 
headSize / 2);
+                       ctx.stroke();
+                       // draw head
+                       ctx.fillStyle = this._hoverColor;
+                       ctx.fillRect(startPar, startOrt, headSize, headSize);
+                       ctx.strokeStyle = 'black';
+                       ctx.lineWidth = 0.5;
+                       ctx.strokeRect(startPar, startOrt, headSize, headSize);
+                       // draw '-'
+                       ctx.lineWidth = 1;
+                       ctx.strokeRect(startPar + headSize / 4, startOrt + 
headSize / 2, headSize / 2, 1);
+               }
+               else {
+                       // draw head
+                       ctx.fillStyle = this._hoverColor;
+                       ctx.fillRect(startPar, startOrt, headSize, headSize);
+                       ctx.strokeStyle = 'black';
+                       ctx.lineWidth = 0.5;
+                       ctx.strokeRect(startPar, startOrt, headSize, headSize);
+                       // draw '+'
+                       ctx.lineWidth = 1;
+                       ctx.beginPath();
+                       ctx.moveTo(startPar + headSize / 4, startOrt + headSize 
/ 2);
+                       ctx.lineTo(startPar + 3 * headSize / 4, startOrt + 
headSize / 2);
+                       ctx.moveTo(startPar + headSize / 2, startOrt + headSize 
/ 4);
+                       ctx.lineTo(startPar + headSize / 2, startOrt + 3 * 
headSize / 4);
+                       ctx.stroke();
+               }
+               ctx.restore();
+       },
+
+       drawLevelHeader: function(level) {
+               var ctx = this._cornerCanvasContext;
+               var ctrlHeadSize = this._groupHeadSize;
+               var levelSpacing = this._levelSpacing;
+
+               var startOrt = levelSpacing + (ctrlHeadSize + levelSpacing) * 
level;
+               var startPar = this._cornerCanvas.width - (ctrlHeadSize + 
(L.Control.Header.rowHeaderWidth - ctrlHeadSize) / 2);
+
+               ctx.save();
+               ctx.fillStyle = this._hoverColor;
+               ctx.fillRect(startPar, startOrt, ctrlHeadSize, ctrlHeadSize);
+               ctx.strokeStyle = 'black';
+               ctx.lineWidth = 0.5;
+               ctx.strokeRect(startPar, startOrt, ctrlHeadSize, ctrlHeadSize);
+               // draw level number
+               ctx.fillStyle = this._textColor;
+               ctx.font = this._font;
+               ctx.textAlign = 'center';
+               ctx.textBaseline = 'middle';
+               ctx.fillText(level + 1, startPar + (ctrlHeadSize / 2), startOrt 
+ (ctrlHeadSize / 2));
                ctx.restore();
        },
 
@@ -245,7 +340,7 @@ L.Control.ColumnHeader = L.Control.Header.extend({
                if (!entry)
                        return;
 
-               var rect = this._headerCanvas.getBoundingClientRect();
+               var rect = this._canvas.getBoundingClientRect();
 
                var colStart = entry.pos - entry.size + this._position;
                var colEnd = entry.pos + this._position;
@@ -259,23 +354,23 @@ L.Control.ColumnHeader = L.Control.Header.extend({
 
        viewRowColumnHeaders: function (e) {
                if (e.data.columns && e.data.columns.length > 0) {
-                       this.fillColumns(e.data.columns, e.converter, 
e.context);
+                       this.fillColumns(e.data.columns, e.data.columnGroups, 
e.converter, e.context);
                }
        },
 
-       fillColumns: function (columns, converter, context) {
+       fillColumns: function (columns, colGroups, converter, context) {
                if (columns.length < 2)
                        return;
 
-               var entry, index, iterator, pos, width;
+               var headerEntry, index, iterator, width, pos;
 
-               var canvas = this._headerCanvas;
-               canvas.width = 
parseInt(L.DomUtil.getStyle(this._headersContainer, 'width'));
-               canvas.height = 
parseInt(L.DomUtil.getStyle(this._headersContainer, 'height'));
+               var canvas = this._canvas;
+               this._setCanvasWidth();
+               this._setCanvasHeight();
                this._canvasContext.clearRect(0, 0, canvas.width, 
canvas.height);
 
                // update first header index and reset no more valid variables
-               this._leftmostColumn = parseInt(columns[0].text);
+               this._startHeaderIndex = parseInt(columns[0].text);
                this._current = -1; // no more valid
                this._selection.start = this._selection.end = -1; // no more 
valid
                this._mouseOverEntry = null;
@@ -288,35 +383,57 @@ L.Control.ColumnHeader = L.Control.Header.extend({
                this.converter = L.Util.bind(converter, context);
                this._data.converter = L.Util.bind(this._twipsToPixels, this);
 
+               // create group array
+               this._groupLevels = parseInt(columns[0].groupLevels);
+               this._groups = this._groupLevels ? new Array(this._groupLevels) 
: null;
+
                var startOffsetTw = parseInt(columns[0].size);
-               this._leftOffset = this._twipsToPixels(startOffsetTw);
+               this._startOffset = this._twipsToPixels(startOffsetTw);
 
                this._data.pushBack(0, {pos: startOffsetTw, size: 0});
                var prevPos = startOffsetTw;
                var nextIndex = parseInt(columns[1].text);
                var last = columns.length - 1;
+
                for (iterator = 1; iterator < last; iterator++) {
                        index = nextIndex;
                        pos = parseInt(columns[iterator].size);
                        nextIndex = parseInt(columns[iterator+1].text);
                        width = pos - prevPos;
                        prevPos = Math.round(pos + width * (nextIndex - index - 
1));
-                       index = index - this._leftmostColumn;
-                       entry = {pos: pos, size: width};
-                       this._data.pushBack(index, entry);
+                       index = index - this._startHeaderIndex;
+                       headerEntry = {pos: pos, size: width};
+                       this._data.pushBack(index, headerEntry);
                }
 
-               // setup last header entry
+               // setup last header headerEntry
+               index = nextIndex - this._startHeaderIndex;
                pos = parseInt(columns[last].size);
-               this._data.pushBack(nextIndex - this._leftmostColumn, {pos: 
pos, size: pos - prevPos});
+               width = pos - prevPos;
+               this._data.pushBack(index, {pos: pos, size: pos - width});
+
+               // collect group controls data
+               if (colGroups !== undefined && this._groups) {
+                       this._collectGroupsData(colGroups);
+               }
+
+               if (this._groups) {
+                       this.resize(this._computeOutlineWidth() + 
this._borderWidth + this._headerHeight);
+               }
+               else if (this._canvas.height !== this._headerHeight) {
+                       this.resize(this._headerHeight);
+               }
 
                // draw header
-               entry = this._data.getFirst();
-               while (entry) {
-                       this.drawHeaderEntry(entry, false);
-                       entry = this._data.getNext();
+               headerEntry = this._data.getFirst();
+               while (headerEntry) {
+                       this.drawHeaderEntry(headerEntry, false);
+                       headerEntry = this._data.getNext();
                }
 
+               // draw group controls
+               this.drawOutline();
+
                this.mouseInit(canvas);
 
                L.DomEvent.on(canvas, 'contextmenu', L.DomEvent.preventDefault);
@@ -366,11 +483,14 @@ L.Control.ColumnHeader = L.Control.Header.extend({
                this._map.sendUnoCommand('.uno:SelectColumn ', command);
        },
 
-       _onHeaderClick: function (e) {
+       _onClick: function (e) {
+               if (this._onOutlineMouseEvent(e, this._onGroupControlClick))
+                       return;
+
                if (!this._mouseOverEntry)
                        return;
 
-               var col = this._mouseOverEntry.index + this._leftmostColumn;
+               var col = this._mouseOverEntry.index + this._startHeaderIndex;
 
                var modifier = 0;
                if (e.shiftKey) {
@@ -383,8 +503,22 @@ L.Control.ColumnHeader = L.Control.Header.extend({
                this._selectColumn(col, modifier);
        },
 
-       _onCornerHeaderClick: function() {
-               this._map.sendUnoCommand('.uno:SelectAll');
+       _onCornerHeaderClick: function(e) {
+               var pos = this._mouseEventToCanvasPos(this._cornerCanvas, e);
+
+               if (pos.y > this.getOutlineWidth()) {
+                       this._map.fire('cornerheaderclicked', e);
+                       return;
+               }
+
+               var rowOutlineWidth = this._cornerCanvas.width - 
L.Control.Header.rowHeaderWidth - this._borderWidth;
+               if (pos.x <= rowOutlineWidth) {
+                       // empty rectangle on the left select all
+                       this._map.sendUnoCommand('.uno:SelectAll');
+               }
+
+               var level = this._getGroupLevel(pos.y);
+               this._updateOutlineState(/*is column: */ true, {column: true, 
level: level, index: -1});
        },
 
        _onDialogResult: function (e) {
@@ -435,7 +569,7 @@ L.Control.ColumnHeader = L.Control.Header.extend({
                var clickedColumn = this._mouseOverEntry;
                if (clickedColumn) {
                        var width = clickedColumn.size;
-                       var column = clickedColumn.index + this._leftmostColumn;
+                       var column = clickedColumn.index + 
this._startHeaderIndex;
 
                        if (this._data.isZeroSize(clickedColumn.index + 1)) {
                                column += 1;
@@ -469,7 +603,7 @@ L.Control.ColumnHeader = L.Control.Header.extend({
                        return;
 
                if (clicks === 2) {
-                       var column = this._mouseOverEntry.index + 
this._leftmostColumn;
+                       var column = this._mouseOverEntry.index + 
this._startHeaderIndex;
                        var command = {
                                Col: {
                                        type: 'unsigned short',
@@ -499,9 +633,34 @@ L.Control.ColumnHeader = L.Control.Header.extend({
                }
        },
 
-       _getPos: function (point) {
+       _getParallelPos: function (point) {
                return point.x;
+       },
+
+       _getOrthogonalPos: function (point) {
+               return point.y;
+       },
+
+       resize: function (height) {
+               if (height < this._headerHeight)
+                       return;
+
+               var rowHeader = 
L.DomUtil.get('spreadsheet-header-rows-container');
+               var document = L.DomUtil.get('document-container');
+
+               this._setCornerCanvasHeight(height);
+               var deltaTop = height - this._canvas.height;
+               var rowHdrTop = parseInt(L.DomUtil.getStyle(rowHeader, 'top')) 
+ deltaTop;
+               var docTop = parseInt(L.DomUtil.getStyle(document, 'top')) + 
deltaTop;
+               console.log('resize: height: ' + height + ', deltaTop: ' + 
deltaTop + ', rowHdrTop: ' + rowHdrTop + ', docTop: ' + docTop);
+               L.DomUtil.setStyle(rowHeader, 'top', rowHdrTop + 'px');
+               L.DomUtil.setStyle(document, 'top', docTop + 'px');
+
+               this._setCanvasHeight(height);
+
+               this._map.fire('updatecornerheader');
        }
+
 });
 
 L.control.columnHeader = function (options) {
diff --git a/loleaflet/src/control/Control.Header.js 
b/loleaflet/src/control/Control.Header.js
index d7689ee0..6dac7330 100644
--- a/loleaflet/src/control/Control.Header.js
+++ b/loleaflet/src/control/Control.Header.js
@@ -8,17 +8,42 @@ L.Control.Header = L.Control.extend({
        },
 
        initialize: function () {
+               this._isColumn = undefined;
+
                this.converter = null;
 
-               this._headerCanvas = null;
+               this._canvas = null;
                this._clicks = 0;
                this._current = -1;
                this._selection = {start: -1, end: -1};
                this._mouseOverEntry = null;
                this._lastMouseOverIndex = undefined;
                this._hitResizeArea = false;
+               this._overHeaderArea = false;
 
                this._selectionBackgroundGradient = [ '#3465A4', '#729FCF', 
'#004586' ];
+
+               this._groups = null;
+
+               // group control styles
+               this._groupHeadSize = 12;
+               this._levelSpacing = 1;
+
+               // set up corner header
+               var cornerHeader = 
L.DomUtil.get('spreadsheet-header-corner-container');
+               if (cornerHeader) {
+                       this._cornerHeaderContainer = cornerHeader;
+                       this._cornerCanvas = 
L.DomUtil.get('spreadsheet-header-corner');
+               }
+               else {
+                       var rowColumnFrame = 
L.DomUtil.get('spreadsheet-row-column-frame');
+                       this._cornerHeaderContainer = 
L.DomUtil.createWithId('div', 'spreadsheet-header-corner-container', 
rowColumnFrame);
+                       this._cornerCanvas = L.DomUtil.createWithId('canvas', 
'spreadsheet-header-corner', this._cornerHeaderContainer);
+                       this._setCornerCanvasWidth();
+                       this._setCornerCanvasHeight();
+               }
+               this._cornerCanvasContext = this._cornerCanvas.getContext('2d');
+               this._cornerCanvasContext.clearRect(0, 0, 
this._cornerCanvas.width, this._cornerCanvas.height);
        },
 
        _initHeaderEntryStyles: function (className) {
@@ -184,7 +209,7 @@ L.Control.Header = L.Control.extend({
                this._selection.end = itEnd;
        },
 
-       updateCurrent: function (data, start) {
+       updateCurrent: function (data, start, size) {
                if (!data || data.isEmpty())
                        return;
 
@@ -195,22 +220,29 @@ L.Control.Header = L.Control.extend({
                }
 
                var x0 = 0, x1 = 0;
+               var prevEntry = null;
                var entry = data.getFirst();
+               var zeroSizeEntry = false;
                while (entry) {
                        x0 = entry.pos - entry.size;
                        x1 = entry.pos;
                        if (x0 <= start && start < x1) {
+                               // we have a slim cursor because of a zero size 
entry ?
+                               zeroSizeEntry = size <= 1 && prevEntry && 
prevEntry.size === 0;
                                // when a whole row (column) is selected the 
cell cursor is moved to the first column (row)
                                // but this action should not cause to 
select/unselect anything, on the contrary we end up
                                // with all column (row) header entries 
selected but the one where the cell cursor was
                                // previously placed
                                if (this._selection.start === -1 && 
this._selection.end === -1) {
                                        this.unselect(data.get(this._current));
-                                       this.select(entry);
+                                       // no selection when the cell cursor is 
slim
+                                       if (!zeroSizeEntry)
+                                               this.select(entry);
                                }
-                               this._current = entry.index;
+                               this._current = zeroSizeEntry ? -1 : 
entry.index;
                                break;
                        }
+                       prevEntry = entry;
                        entry = data.getNext();
                }
        },
@@ -224,24 +256,39 @@ L.Control.Header = L.Control.extend({
        },
 
        _onMouseOut: function (e) {
+               if (this._hitOutline(e))
+                       return;
+
+               this._onHeaderMouseOut(e);
+       },
+
+       _onHeaderMouseOut: function (e) {
+               if (!this._overHeaderArea)
+                       return;
+               this._overHeaderArea = false;
+
                if (this._mouseOverEntry) {
-                       this.drawHeaderEntry(this._mouseOverEntry, false);
+                       this.drawHeaderEntry(this._mouseOverEntry, /*isOver: */ 
false);
                        this._lastMouseOverIndex = this._mouseOverEntry.index; 
// used by context menu
                        this._mouseOverEntry = null;
                }
                this._hitResizeArea = false;
-               L.DomUtil.setStyle(this._headerCanvas, 'cursor', this._cursor);
+               L.DomEvent.on(this._canvas, 'click', this._onClick, this);
+               L.DomUtil.setStyle(this._canvas, 'cursor', 'default');
        },
 
-       _onCanvasMouseMove: function (e) {
-               var target = e.target || e.srcElement;
-
-               if (!target || this._dragging) {
+       _onMouseMove: function (e) {
+               if (this._hitOutline(e)) {
+                       this._onHeaderMouseOut(e);
                        return false;
                }
+               if (!this._overHeaderArea) {
+                       L.DomUtil.setStyle(this._canvas, 'cursor', 
this._cursor);
+                       this._overHeaderArea = true;
+               }
 
                var isMouseOverResizeArea = false;
-               var pos = 
this._getPos(this._mouseEventToCanvasPos(this._headerCanvas, e));
+               var pos = 
this._getParallelPos(this._mouseEventToCanvasPos(this._canvas, e));
                pos = pos - this._position;
 
                var mouseOverIndex = this._mouseOverEntry ? 
this._mouseOverEntry.index : undefined;
@@ -266,20 +313,91 @@ L.Control.Header = L.Control.extend({
 
                if (isMouseOverResizeArea !== this._hitResizeArea) {
                        if (isMouseOverResizeArea) {
-                               L.DomEvent.off(this._headerCanvas, 'click', 
this._onHeaderClick, this);
+                               L.DomEvent.off(this._canvas, 'click', 
this._onClick, this);
                        }
                        else {
-                               L.DomEvent.on(this._headerCanvas, 'click', 
this._onHeaderClick, this);
+                               L.DomEvent.on(this._canvas, 'click', 
this._onClick, this);
                        }
                        var cursor = isMouseOverResizeArea ? this._resizeCursor 
: this._cursor;
-                       L.DomUtil.setStyle(this._headerCanvas, 'cursor', 
cursor);
+                       L.DomUtil.setStyle(this._canvas, 'cursor', cursor);
                        this._hitResizeArea = isMouseOverResizeArea;
                }
        },
 
+
+       _onOutlineMouseEvent: function (e, eventHandler) {
+               // check if the group controls area has been hit
+               if (!this._hitOutline(e))
+                       return false;
+
+               var pos = this._mouseEventToCanvasPos(this._canvas, e);
+               var level = this._getGroupLevel(this._getOrthogonalPos(pos));
+               if (level < 0 || level >= this._groups.length)
+                       return true;
+
+               // when 2 collapsed group controls overlaps completely,
+               // clicking on the control should expand the lower/rightmost 
group
+               var groups = this._groups[level];
+               var indexes = Object.keys(groups);
+               var len = indexes.length;
+               for (var i = len - 1; i >= 0; --i) {
+                       e.group = groups[indexes[i]];
+                       if (eventHandler.call(this, e))
+                               break;
+               }
+
+               return true;
+       },
+
+       _onGroupControlClick: function (e) {
+               var group = e.group;
+               if (!group)
+                       return false;
+
+               var pos = 
this._getParallelPos(this._mouseEventToCanvasPos(this._canvas, e));
+               pos = pos - this._position;
+               if (group.startPos < pos && pos < group.startPos + 
this._groupHeadSize) {
+                       this._updateOutlineState(/*isColumnOutline: */ 
this._isColumn, group);
+                       return true;
+               }
+               return false;
+       },
+
+       _onDoubleClick: function (e) {
+               this._onOutlineMouseEvent(e, this._onGroupControlDoubleClick);
+       },
+
+       _onGroupControlDoubleClick: function (e) {
+               var group = e.group;
+               if (!group && !group.hidden)
+                       return false;
+
+               var pos = 
this._getParallelPos(this._mouseEventToCanvasPos(this._canvas, e));
+               pos = pos - this._position;
+               if (group.startPos + this._groupHeadSize < pos && pos < 
group.endPos) {
+                       this._updateOutlineState(/*isColumnOutline: */ 
this._isColumn, group);
+                       return true;
+               }
+               return false;
+       },
+
+       _updateOutlineState: function (column, group) {
+               var e = {
+                       x: this._map._getTopLeftPoint().x,
+                       y: this._map._getTopLeftPoint().y,
+                       offset: {x: undefined, y: undefined},
+                       outline: {column: column, level: group.level, index: 
group.index, hidden: !group.hidden}
+               };
+               this._map.fire('updaterowcolumnheaders', e);
+               // TODO do we need this ?
+               //this._map._socket.sendMessage('commandvalues 
command=.uno:ViewAnnotationsPosition');
+       },
+
        _onMouseDown: function (e) {
-               var target = e.target || e.srcElement;
+               if (this._hitOutline(e))
+                       return;
 
+               var target = e.target || e.srcElement;
                if (!target || this._dragging) {
                        return false;
                }
@@ -292,10 +410,11 @@ L.Control.Header = L.Control.extend({
 
                L.DomEvent.stopPropagation(e);
 
-               L.DomEvent.off(target, 'mousemove', this._onCanvasMouseMove, 
this);
+               // disable normal mouse events
+               L.DomEvent.off(target, 'mousemove', this._onMouseMove, this);
                L.DomEvent.off(target, 'mouseout', this._onMouseOut, this);
-
-               L.DomEvent.on(document, 'mousemove', this._onMouseMove, this);
+               // enable mouse events used on dragging
+               L.DomEvent.on(document, 'mousemove', 
this._onMouseMoveForDragging, this);
                L.DomEvent.on(document, 'mouseup', this._onMouseUp, this);
 
                var rect = this.getHeaderEntryBoundingClientRect();
@@ -306,7 +425,7 @@ L.Control.Header = L.Control.extend({
                this.onDragStart(this.item, this._start, this._offset, e);
        },
 
-       _onMouseMove: function (e) {
+       _onMouseMoveForDragging: function (e) {
                this._dragging = true;
                L.DomEvent.preventDefault(e);
 
@@ -314,13 +433,14 @@ L.Control.Header = L.Control.extend({
        },
 
        _onMouseUp: function (e) {
-               L.DomEvent.off(document, 'mousemove', this._onMouseMove, this);
+               // disable mouse events used on dragging
+               L.DomEvent.off(document, 'mousemove', 
this._onMouseMoveForDragging, this);
                L.DomEvent.off(document, 'mouseup', this._onMouseUp, this);
 
                L.DomUtil.enableImageDrag();
                L.DomUtil.enableTextSelection();
-
-               L.DomEvent.on(this._item, 'mousemove', this._onCanvasMouseMove, 
this);
+               // enable normal mouse events
+               L.DomEvent.on(this._item, 'mousemove', this._onMouseMove, this);
                L.DomEvent.on(this._item, 'mouseout', this._onMouseOut, this);
 
                if (this._dragging) {
@@ -339,7 +459,181 @@ L.Control.Header = L.Control.extend({
                if (!this.converter)
                        return 0;
                var point = new L.Point(twips, twips);
-               return Math.round(this._getPos(this.converter(point)));
+               return Math.round(this._getParallelPos(this.converter(point)));
+       },
+
+       _setCanvasSizeImpl: function (container, canvas, property, value) {
+               if (!value) {
+                       value = parseInt(L.DomUtil.getStyle(container, 
property));
+               }
+               else {
+                       L.DomUtil.setStyle(container, property, value + 'px');
+               }
+               canvas[property] = value;
+       },
+
+       _setCanvasWidth: function (width) {
+               this._setCanvasSizeImpl(this._headerContainer, this._canvas, 
'width', width);
+       },
+
+       _setCanvasHeight: function (height) {
+               this._setCanvasSizeImpl(this._headerContainer, this._canvas, 
'height', height);
+       },
+
+       _setCornerCanvasWidth: function (width) {
+               this._setCanvasSizeImpl(this._cornerHeaderContainer, 
this._cornerCanvas, 'width', width);
+       },
+
+       _setCornerCanvasHeight: function (height) {
+               this._setCanvasSizeImpl(this._cornerHeaderContainer, 
this._cornerCanvas, 'height', height);
+       },
+
+       _hitOutline: function (e) {
+               var pos = this._mouseEventToCanvasPos(this._canvas, e);
+               return this._getOrthogonalPos(pos) <= this.getOutlineWidth();
+       },
+
+       _getGroupLevel: function (pos) {
+               var levels = this._groups.length;
+               var size = this._levelSpacing + this._groupHeadSize;
+
+               var level = (pos + 1) / size | 0;
+               var relPos = pos % size;
+
+               if (level <= levels && relPos > this._levelSpacing) {
+                       return level;
+               }
+               else {
+                       return -1;
+               }
+       },
+
+       _getGroupLevelHeader: function (pos) {
+               if (!this._groups)
+                       return;
+
+               var levels = this._groups.length + 1;
+               var size = this._levelSpacing + this._groupHeadSize;
+
+               var level = (pos + 1) / size | 0;
+               var relPos = pos % size;
+
+               if (level < this._groups.length && relPos > this._levelSpacing) 
{
+                       return level;
+               }
+               else {
+                       return -1;
+               }
+       },
+
+       _computeOutlineWidth: function () {
+               return this._levelSpacing + (this._groupHeadSize + 
this._levelSpacing) * (this._groups.length + 1);
+       },
+
+       getOutlineWidth: function () {
+               if (this._isColumn)
+                       return this._canvas.height - this._borderWidth - 
this._headerHeight;
+               else
+                       return this._canvas.width - this._borderWidth - 
this._headerWidth;
+       },
+
+       _collectGroupsData: function(groups) {
+               var level, groupEntry;
+
+               var lastGroupIndex = new Array(groups.length);
+               var firstChildGroupIndex = new Array(groups.length);
+               var lastLevel = -1;
+               for (var i = 0; i < groups.length; ++i) {
+                       // a new group start
+                       var groupData = groups[i];
+                       level = parseInt(groupData.level) - 1;
+                       if (!this._groups[level]) {
+                               this._groups[level] = {};
+                       }
+                       var startPos = 
this._twipsToPixels(parseInt(groupData.startPos));
+                       var endPos = 
this._twipsToPixels(parseInt(groupData.endPos));
+                       var isHidden = !!parseInt(groupData.hidden);
+                       if (isHidden) {
+                               startPos -= this._groupHeadSize / 2;
+                               endPos = startPos + this._groupHeadSize;
+                       }
+                       else {
+                               var moved = false;
+                               // if the first child is collapsed the parent 
head has to be top-aligned with the child
+                               if (level < lastLevel && 
firstChildGroupIndex[lastLevel] !== undefined) {
+                                       var childGroupEntry = 
this._groups[lastLevel][firstChildGroupIndex[lastLevel]];
+                                       if (childGroupEntry.hidden) {
+                                               if (startPos > 
childGroupEntry.startPos && startPos < childGroupEntry.endPos) {
+                                                       startPos = 
childGroupEntry.startPos;
+                                                       moved = true;
+                                               }
+                                       }
+                               }
+                               // if 2 groups belonging to the same level are 
contiguous and the first group is collapsed,
+                               // the second one has to be shifted as much as 
possible in order to avoiding overlapping.
+                               if (!moved && lastGroupIndex[level] !== 
undefined) {
+                                       var prevGroupEntry = 
this._groups[level][lastGroupIndex[level]];
+                                       if (prevGroupEntry.hidden) {
+                                               if (startPos > 
prevGroupEntry.startPos && startPos < prevGroupEntry.endPos) {
+                                                       startPos = 
prevGroupEntry.endPos;
+                                               }
+                                       }
+                               }
+                       }
+                       groupEntry = {
+                               level: level,
+                               index: groupData.index,
+                               startPos: startPos,
+                               endPos: endPos,
+                               hidden: isHidden
+                       };
+                       this._groups[level][groupData.index] = groupEntry;
+                       lastGroupIndex[level] = groupData.index;
+                       if (level > lastLevel) {
+                               firstChildGroupIndex[level] = groupData.index;
+                               lastLevel = level;
+                       }
+                       else if (level === lastLevel) {
+                               firstChildGroupIndex[level + 1] = undefined;
+                       }
+               }
+       },
+
+       drawCornerHeader: function() {
+               var ctx = this._cornerCanvasContext;
+
+               if (!this._groups)
+                       return;
+
+               ctx.save();
+               ctx.fillStyle = this._borderColor;
+               if (this._isColumn) {
+                       var startY = this._cornerCanvas.height - 
(L.Control.Header.colHeaderHeight + this._borderWidth);
+                       if (startY > 0)
+                               ctx.fillRect(0, startY, 
this._cornerCanvas.width, this._borderWidth);
+               }
+               else {
+                       var startX = this._cornerCanvas.width - 
(L.Control.Header.rowHeaderWidth + this._borderWidth);
+                       if (startX > 0)
+                               ctx.fillRect(startX, 0, this._borderWidth, 
this._cornerCanvas.height);
+               }
+               ctx.restore();
+
+               var levels = this._groups.length + 1;
+               for (var i = 0; i < levels; ++i) {
+                       this.drawLevelHeader(i);
+               }
+       },
+
+       drawOutline: function() {
+               if (this._groups) {
+                       for (var itLevel = 0; itLevel < this._groups.length; 
++itLevel) {
+                               for (var groupIndex in this._groups[itLevel]) {
+                                       if 
(this._groups[itLevel].hasOwnProperty(groupIndex))
+                                               
this.drawGroupControl(this._groups[itLevel][groupIndex]);
+                               }
+                       }
+               }
        },
 
        onDragStart: function () {},
@@ -348,10 +642,15 @@ L.Control.Header = L.Control.extend({
        onDragClick: function () {},
        getHeaderEntryBoundingClientRect: function () {},
        drawHeaderEntry: function () {},
-       _getPos: function () {}
+       drawGroupControl: function () {},
+       _getParallelPos: function () {},
+       _getOrthogonalPos: function () {}
+
 });
 
 (function () {
+       L.Control.Header.rowHeaderWidth = undefined;
+       L.Control.Header.colHeaderHeight = undefined;
 
        L.Control.Header.DataImpl = L.Class.extend({
                initialize: function () {
diff --git a/loleaflet/src/control/Control.Menubar.js 
b/loleaflet/src/control/Control.Menubar.js
index eb825cfe..28d5491f 100644
--- a/loleaflet/src/control/Control.Menubar.js
+++ b/loleaflet/src/control/Control.Menubar.js
@@ -311,6 +311,15 @@ L.Control.Menubar = L.Control.extend({
                                {name: _('Delete row'), type: 'unocommand', 
uno: '.uno:DeleteRows'},
                                {name: _('Delete column'), type: 'unocommand', 
uno: '.uno:DeleteColumns'}]
                        },
+                       {name: _('Data'), type: 'menu', menu: [
+                               {name: _('Group'), type: 'unocommand', uno: 
'.uno:Group'},
+                               {name: _('Ungroup'), type: 'unocommand', uno: 
'.uno:Ungroup'},
+                               {type: 'separator'},
+                               {name: _('Remove Outline'), type: 'unocommand', 
uno: '.uno:ClearOutline'},
+                               {type: 'separator'},
+                               {name: _('Show Details'), type: 'unocommand', 
uno: '.uno:ShowDetail'},
+                               {name: _('Hide Details'), type: 'unocommand', 
uno: '.uno:HideDetail'}]
+                       },
                        {name: _('Tools'), id: 'tools', type: 'menu', menu: [
                                {name: _('Automatic spell checking'), type: 
'unocommand', uno: '.uno:SpellOnline'},
                                {name: _('Language'), type: 'menu', menu: [
diff --git a/loleaflet/src/control/Control.RowHeader.js 
b/loleaflet/src/control/Control.RowHeader.js
index 4763f1b1..1eed7cd0 100644
--- a/loleaflet/src/control/Control.RowHeader.js
+++ b/loleaflet/src/control/Control.RowHeader.js
@@ -15,34 +15,39 @@ L.Control.RowHeader = L.Control.Header.extend({
 
        _initialize: function () {
                this._initialized = true;
+               this._isColumn = false;
                this._map.on('scrolloffset', this.offsetScrollPosition, this);
                this._map.on('updatescrolloffset', this.setScrollPosition, 
this);
                this._map.on('viewrowcolumnheaders', this.viewRowColumnHeaders, 
this);
                this._map.on('updateselectionheader', this._onUpdateSelection, 
this);
                this._map.on('clearselectionheader', this._onClearSelection, 
this);
                this._map.on('updatecurrentheader', this._onUpdateCurrentRow, 
this);
+               this._map.on('updatecornerheader', this.drawCornerHeader, this);
+               this._map.on('cornerheaderclicked', this._onCornerHeaderClick, 
this);
                var rowColumnFrame = 
L.DomUtil.get('spreadsheet-row-column-frame');
-               this._headersContainer = L.DomUtil.create('div', 
'spreadsheet-header-rows-container', rowColumnFrame);
-
-               this._headerCanvas = L.DomUtil.create('canvas', 
'spreadsheet-header-rows', this._headersContainer);
+               this._headerContainer = L.DomUtil.createWithId('div', 
'spreadsheet-header-rows-container', rowColumnFrame);
 
                this._initHeaderEntryStyles('spreadsheet-header-row');
                
this._initHeaderEntryHoverStyles('spreadsheet-header-row-hover');
                
this._initHeaderEntrySelectedStyles('spreadsheet-header-row-selected');
                
this._initHeaderEntryResizeStyles('spreadsheet-header-row-resize');
 
-               this._canvasContext = this._headerCanvas.getContext('2d');
-               this._headerCanvas.width = 
parseInt(L.DomUtil.getStyle(this._headersContainer, 'width'));
-               this._headerCanvas.height = 
parseInt(L.DomUtil.getStyle(this._headersContainer, 'height'));
+               this._canvas = L.DomUtil.create('canvas', 
'spreadsheet-header-rows', this._headerContainer);
+               this._canvasContext = this._canvas.getContext('2d');
+               this._setCanvasWidth();
+               this._setCanvasHeight();
+               this._headerWidth = this._canvas.width;
+               L.Control.Header.rowHeaderWidth = this._canvas.width;
 
-               L.DomUtil.setStyle(this._headerCanvas, 'cursor', this._cursor);
+               L.DomUtil.setStyle(this._canvas, 'cursor', this._cursor);
 
-               L.DomEvent.on(this._headerCanvas, 'mousemove', 
this._onCanvasMouseMove, this);
-               L.DomEvent.on(this._headerCanvas, 'mouseout', this._onMouseOut, 
this);
-               L.DomEvent.on(this._headerCanvas, 'click', this._onHeaderClick, 
this);
+               L.DomEvent.on(this._canvas, 'mousemove', this._onMouseMove, 
this);
+               L.DomEvent.on(this._canvas, 'mouseout', this._onMouseOut, this);
+               L.DomEvent.on(this._canvas, 'click', this._onClick, this);
+               L.DomEvent.on(this._canvas, 'dblclick', this._onDoubleClick, 
this);
 
-               this._topRow = 0;
-               this._topOffset = 0;
+               this._startHeaderIndex = 0;
+               this._startOffset = 0;
                this._position = 0;
 
                var rowHeaderObj = this;
@@ -170,11 +175,13 @@ L.Control.RowHeader = L.Control.Header.extend({
        },
 
        _onUpdateCurrentRow: function (e) {
-               var y = e.y;
+               var y = e.min.y;
+               var h = e.getSize().y;
                if (y !== -1) {
                        y = this._twipsToPixels(y);
+                       h = this._twipsToPixels(h);
                }
-               this.updateCurrent(this._data, y);
+               this.updateCurrent(this._data, y, h);
        },
 
        _updateRowHeader: function () {
@@ -186,11 +193,12 @@ L.Control.RowHeader = L.Control.Header.extend({
                        return;
 
                var ctx = this._canvasContext;
-               var content = entry.index + this._topRow;
-               var start = entry.pos - entry.size - this._topOffset;
-               var end = entry.pos - this._topOffset;
-               var height = end - start;
-               var width = this._headerCanvas.width;
+               var content = entry.index + this._startHeaderIndex;
+               var startOrt = this._canvas.width - this._headerWidth;
+               var startPar = entry.pos - entry.size - this._startOffset;
+               var endPar = entry.pos - this._startOffset;
+               var height = endPar - startPar;
+               var width = this._headerWidth;
 
                if (isHighlighted !== true && isHighlighted !== false) {
                        isHighlighted = this.isHighlighted(entry.index);
@@ -200,31 +208,118 @@ L.Control.RowHeader = L.Control.Header.extend({
                        return;
 
                ctx.save();
-               ctx.translate(0, this._position + this._topOffset);
+               ctx.translate(0, this._position + this._startOffset);
                // background gradient
                var selectionBackgroundGradient = null;
                if (isHighlighted) {
-                       selectionBackgroundGradient = 
ctx.createLinearGradient(0, start, 0, start + height);
+                       selectionBackgroundGradient = 
ctx.createLinearGradient(0, startPar, 0, startPar + height);
                        selectionBackgroundGradient.addColorStop(0, 
this._selectionBackgroundGradient[0]);
                        selectionBackgroundGradient.addColorStop(0.5, 
this._selectionBackgroundGradient[1]);
                        selectionBackgroundGradient.addColorStop(1, 
this._selectionBackgroundGradient[2]);
                }
+
+               // draw header/outline border separator
+               if (this._headerWidth !== this._canvas.width) {
+                       ctx.fillStyle = this._borderColor;
+                       ctx.fillRect(startOrt - this._borderWidth, startPar, 
this._borderWidth, height);
+               }
+
                // clip mask
                ctx.beginPath();
-               ctx.rect(0, start, width, height);
+               ctx.rect(startOrt, startPar, width, height);
                ctx.clip();
                // draw background
                ctx.fillStyle = isHighlighted ? selectionBackgroundGradient : 
isOver ? this._hoverColor : this._backgroundColor;
-               ctx.fillRect(0, start, width, height);
+               ctx.fillRect(startOrt, startPar, width, height);
                // draw text content
                ctx.fillStyle = isHighlighted ? this._selectionTextColor : 
this._textColor;
                ctx.font = this._font;
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
-               ctx.fillText(content, width / 2, end - (height / 2));
+               ctx.fillText(content, startOrt + (width / 2), endPar - (height 
/ 2));
                // draw row separator
                ctx.fillStyle = this._borderColor;
-               ctx.fillRect(0, end -1, width, this._borderWidth);
+               ctx.fillRect(startOrt, endPar - 1, width , this._borderWidth);
+               ctx.restore();
+       },
+
+       drawGroupControl: function (group) {
+               if (!group)
+                       return;
+
+               var ctx = this._canvasContext;
+               var headSize = this._groupHeadSize;
+               var spacing = this._levelSpacing;
+               var level = group.level;
+
+               var startOrt = spacing + (headSize + spacing) * level;
+               var startPar = group.startPos - this._startOffset;
+               var height = group.endPos - group.startPos;
+
+               ctx.save();
+               ctx.translate(0, this._position + this._startOffset);
+               // clip mask
+               ctx.beginPath();
+               ctx.rect(startOrt, startPar, headSize, height);
+               ctx.clip();
+               if (!group.hidden) {
+                       //draw tail
+                       ctx.strokeStyle = 'black';
+                       ctx.lineWidth = 1.5;
+                       ctx.beginPath();
+                       ctx.moveTo(startOrt + 2, startPar + headSize);
+                       ctx.lineTo(startOrt + 2, startPar + height - 1);
+                       ctx.lineTo(startOrt + 2 + headSize / 2, startPar + 
height - 1);
+                       ctx.stroke();
+                       // draw head
+                       ctx.fillStyle = this._hoverColor;
+                       ctx.fillRect(startOrt, startPar, headSize, headSize);
+                       ctx.strokeStyle = 'black';
+                       ctx.lineWidth = 0.5;
+                       ctx.strokeRect(startOrt, startPar, headSize, headSize);
+                       // draw '-'
+                       ctx.lineWidth = 1;
+                       ctx.strokeRect(startOrt + headSize / 4, startPar + 
headSize / 2, headSize / 2, 1);
+               }
+               else {
+                       // draw head
+                       ctx.fillStyle = this._hoverColor;
+                       ctx.fillRect(startOrt, startPar, headSize, headSize);
+                       ctx.strokeStyle = 'black';
+                       ctx.lineWidth = 0.5;
+                       ctx.strokeRect(startOrt, startPar, headSize, headSize);
+                       // draw '+'
+                       ctx.lineWidth = 1;
+                       ctx.beginPath();
+                       ctx.moveTo(startOrt + headSize / 4, startPar + headSize 
/ 2);
+                       ctx.lineTo(startOrt + 3 * headSize / 4, startPar + 
headSize / 2);
+                       ctx.moveTo(startOrt + headSize / 2, startPar + headSize 
/ 4);
+                       ctx.lineTo(startOrt + headSize / 2, startPar + 3 * 
headSize / 4);
+                       ctx.stroke();
+               }
+               ctx.restore();
+       },
+
+       drawLevelHeader: function(level) {
+               var ctx = this._cornerCanvasContext;
+               var ctrlHeadSize = this._groupHeadSize;
+               var levelSpacing = this._levelSpacing;
+
+               var startOrt = levelSpacing + (ctrlHeadSize + levelSpacing) * 
level;
+               var startPar = this._cornerCanvas.height - (ctrlHeadSize + 
(L.Control.Header.colHeaderHeight - ctrlHeadSize) / 2);
+
+               ctx.save();
+               ctx.fillStyle = this._hoverColor;
+               ctx.fillRect(startOrt, startPar, ctrlHeadSize, ctrlHeadSize);
+               ctx.strokeStyle = 'black';
+               ctx.lineWidth = 0.5;
+               ctx.strokeRect(startOrt, startPar, ctrlHeadSize, ctrlHeadSize);
+               // draw level number
+               ctx.fillStyle = this._textColor;
+               ctx.font = this._font;
+               ctx.textAlign = 'center';
+               ctx.textBaseline = 'middle';
+               ctx.fillText(level + 1, startOrt + (ctrlHeadSize / 2), startPar 
+ (ctrlHeadSize / 2));
                ctx.restore();
        },
 
@@ -236,7 +331,7 @@ L.Control.RowHeader = L.Control.Header.extend({
                if (!entry)
                        return;
 
-               var rect = this._headerCanvas.getBoundingClientRect();
+               var rect = this._canvas.getBoundingClientRect();
 
                var rowStart = entry.pos - entry.size + this._position;
                var rowEnd = entry.pos + this._position;
@@ -250,23 +345,23 @@ L.Control.RowHeader = L.Control.Header.extend({
 
        viewRowColumnHeaders: function (e) {
                if (e.data.rows && e.data.rows.length) {
-                       this.fillRows(e.data.rows, e.converter, e.context);
+                       this.fillRows(e.data.rows, e.data.rowGroups, 
e.converter, e.context);
                }
        },
 
-       fillRows: function (rows, converter, context) {
+       fillRows: function (rows, rowGroups, converter, context) {
                if (rows.length < 2)
                        return;
 
-               var entry, index, iterator, height, pos;
+               var headerEntry, index, iterator, height, pos;
 
-               var canvas = this._headerCanvas;
-               canvas.width = 
parseInt(L.DomUtil.getStyle(this._headersContainer, 'width'));
-               canvas.height = 
parseInt(L.DomUtil.getStyle(this._headersContainer, 'height'));
+               var canvas = this._canvas;
+               this._setCanvasWidth();
+               this._setCanvasHeight();
                this._canvasContext.clearRect(0, 0, canvas.width, 
canvas.height);
 
                // update first header index and reset no more valid variables
-               this._topRow = parseInt(rows[0].text);
+               this._startHeaderIndex = parseInt(rows[0].text);
                this._current = -1;
                this._selection.start = this._selection.end = -1;
                this._mouseOverEntry = null;
@@ -279,8 +374,12 @@ L.Control.RowHeader = L.Control.Header.extend({
                this.converter = L.Util.bind(converter, context);
                this._data.converter = L.Util.bind(this._twipsToPixels, this);
 
+               // create group array
+               this._groupLevels = parseInt(rows[0].groupLevels);
+               this._groups = this._groupLevels ? new Array(this._groupLevels) 
: null;
+
                var startOffsetTw = parseInt(rows[0].size);
-               this._topOffset = this._twipsToPixels(startOffsetTw);
+               this._startOffset = this._twipsToPixels(startOffsetTw);
 
                this._data.pushBack(0, {pos: startOffsetTw, size: 0});
                var prevPos = startOffsetTw;
@@ -293,24 +392,39 @@ L.Control.RowHeader = L.Control.Header.extend({
                        nextIndex = parseInt(rows[iterator+1].text);
                        height = pos - prevPos;
                        prevPos = Math.round(pos + height * (nextIndex - index 
- 1));
-                       index = index - this._topRow;
-                       entry = {pos: pos, size: height};
-                       this._data.pushBack(index, entry);
+                       index = index - this._startHeaderIndex;
+                       headerEntry = {pos: pos, size: height};
+                       this._data.pushBack(index, headerEntry);
                }
 
                // setup last header entry
-               index = nextIndex - this._topRow;
+               index = nextIndex - this._startHeaderIndex;
                pos = parseInt(rows[last].size);
                height = pos - prevPos;
                this._data.pushBack(index, {pos: pos, size: height});
 
+               // collect group controls data
+               if (rowGroups !== undefined && this._groups) {
+                       this._collectGroupsData(rowGroups);
+               }
+
+               if (this._groups) {
+                       this.resize(this._computeOutlineWidth() + 
this._borderWidth + this._headerWidth);
+               }
+               else if (this._canvas.width !== this._headerWidth) {
+                       this.resize(this._headerWidth);
+               }
+
                // draw header
-               entry = this._data.getFirst();
-               while (entry) {
-                       this.drawHeaderEntry(entry, false);
-                       entry = this._data.getNext();
+               headerEntry = this._data.getFirst();
+               while (headerEntry) {
+                       this.drawHeaderEntry(headerEntry, false);
+                       headerEntry = this._data.getNext();
                }
 
+               // draw group controls
+               this.drawOutline();
+
                this.mouseInit(canvas);
 
                L.DomEvent.on(canvas, 'contextmenu', L.DomEvent.preventDefault);
@@ -334,11 +448,14 @@ L.Control.RowHeader = L.Control.Header.extend({
                this._map.sendUnoCommand('.uno:SelectRow ', command);
        },
 
-       _onHeaderClick: function (e) {
+       _onClick: function (e) {
+               if (this._onOutlineMouseEvent(e, this._onGroupControlClick))
+                       return;
+
                if (!this._mouseOverEntry)
                        return;
 
-               var row = this._mouseOverEntry.index + this._topRow;
+               var row = this._mouseOverEntry.index + this._startHeaderIndex;
 
                var modifier = 0;
                if (e.shiftKey) {
@@ -351,6 +468,18 @@ L.Control.RowHeader = L.Control.Header.extend({
                this._selectRow(row, modifier);
        },
 
+       _onCornerHeaderClick: function(e) {
+               var pos = this._mouseEventToCanvasPos(this._cornerCanvas, e);
+
+               if (pos.x > this.getOutlineWidth()) {
+                       // empty rectangle on the right select all
+                       this._map.sendUnoCommand('.uno:SelectAll');
+               }
+
+               var level = this._getGroupLevel(pos.x);
+               this._updateOutlineState(/*is column: */ false, {column: false, 
level: level, index: -1});
+       },
+
        _onDialogResult: function (e) {
                if (e.type === 'submit' && !isNaN(e.value)) {
                        var extra = {
@@ -399,7 +528,7 @@ L.Control.RowHeader = L.Control.Header.extend({
                var clickedRow = this._mouseOverEntry;
                if (clickedRow) {
                        var height = clickedRow.size;
-                       var row = clickedRow.index + this._topRow;
+                       var row = clickedRow.index + this._startHeaderIndex;
 
                        if (this._data.isZeroSize(clickedRow.index + 1)) {
                                row += 1;
@@ -432,7 +561,7 @@ L.Control.RowHeader = L.Control.Header.extend({
                        return;
 
                if (clicks === 2) {
-                       var row = this._mouseOverEntry.index + this._topRow;
+                       var row = this._mouseOverEntry.index + 
this._startHeaderIndex;
                        var command = {
                                Row: {
                                        type: 'long',
@@ -470,8 +599,33 @@ L.Control.RowHeader = L.Control.Header.extend({
                }
        },
 
-       _getPos: function (point) {
+       _getParallelPos: function (point) {
                return point.y;
+       },
+
+       _getOrthogonalPos: function (point) {
+               return point.x;
+       },
+
+       resize: function (width) {
+               if (width < this._headerWidth)
+                       return;
+
+               var columnHeader = 
L.DomUtil.get('spreadsheet-header-columns-container');
+               var document = L.DomUtil.get('document-container');
+
+               this._setCornerCanvasWidth(width);
+
+               var deltaLeft = width - this._canvas.width;
+               var colHdrLeft = parseInt(L.DomUtil.getStyle(columnHeader, 
'left')) + deltaLeft;
+               var docLeft = parseInt(L.DomUtil.getStyle(document, 'left')) + 
deltaLeft;
+               console.log('resize: width: ' + width + ', deltaLeft: ' + 
deltaLeft + ', colHdrLeft: ' + colHdrLeft + ', docLeft: ' + docLeft);
+               L.DomUtil.setStyle(columnHeader, 'left', colHdrLeft + 'px');
+               L.DomUtil.setStyle(document, 'left', docLeft + 'px');
+
+               this._setCanvasWidth(width);
+
+               this._map.fire('updatecornerheader');
        }
 });
 
diff --git a/loleaflet/src/control/Control.Scroll.js 
b/loleaflet/src/control/Control.Scroll.js
index 06dfae20..b5e3b1ef 100644
--- a/loleaflet/src/control/Control.Scroll.js
+++ b/loleaflet/src/control/Control.Scroll.js
@@ -275,6 +275,11 @@ L.Control.Scroll = L.Control.extend({
                var payload = 'commandvalues 
command=.uno:ViewRowColumnHeaders?x=' + Math.round(pos.x) + '&y=' + 
Math.round(pos.y) +
                        '&width=' + Math.round(size.x) + '&height=' + 
Math.round(size.y);
 
+               if (e.outline) {
+                       payload += '&columnOutline=' + e.outline.column + 
'&groupLevel=' + e.outline.level
+                               + '&groupIndex=' + e.outline.index + 
'&groupHidden=' + e.outline.hidden;
+               }
+
                this._map._socket.sendMessage(payload);
        }
 });
diff --git a/loleaflet/src/dom/DomUtil.js b/loleaflet/src/dom/DomUtil.js
index 012c2574..a3e941a5 100644
--- a/loleaflet/src/dom/DomUtil.js
+++ b/loleaflet/src/dom/DomUtil.js
@@ -35,6 +35,18 @@ L.DomUtil = {
                return el;
        },
 
+       createWithId: function (tagName, id, container) {
+
+               var el = document.createElement(tagName);
+               el.id = id;
+
+               if (container) {
+                       container.appendChild(el);
+               }
+
+               return el;
+       },
+
        remove: function (el) {
                var parent = el.parentNode;
                if (parent) {
diff --git a/loleaflet/src/layer/tile/CalcTileLayer.js 
b/loleaflet/src/layer/tile/CalcTileLayer.js
index a863e4a1..b15cd5bb 100644
--- a/loleaflet/src/layer/tile/CalcTileLayer.js
+++ b/loleaflet/src/layer/tile/CalcTileLayer.js
@@ -356,10 +356,12 @@ L.CalcTileLayer = L.TileLayer.extend({
 
        _onUpdateCurrentHeader: function() {
                var pos = new L.Point(-1, -1);
+               var size = new L.Point(-1, -1);
                if (this._cellCursor && 
!this._isEmptyRectangle(this._cellCursor)) {
                        pos = this._cellCursorTwips.min.add([1, 1]);
+                       size = this._cellCursorTwips.getSize();
                }
-               this._map.fire('updatecurrentheader', pos);
+               this._map.fire('updatecurrentheader', new L.Bounds(pos, 
pos.add(size)));
        },
 
        _onUpdateSelectionHeader: function () {
_______________________________________________
Libreoffice-commits mailing list
libreoffice-comm...@lists.freedesktop.org
https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits

Reply via email to