[Libreoffice-commits] online.git: Branch 'distro/collabora/collabora-online-3-0' - loleaflet/dist loleaflet/src

Marco Cecchetti marco.cecchetti at collabora.com
Wed Nov 29 13:52:16 UTC 2017


 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 6311305822aec8d1e94212a1a98f188b850be0eb
Author: Marco Cecchetti <marco.cecchetti at 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/45418
    Reviewed-by: Jan Holesovsky <kendy at collabora.com>
    Tested-by: Jan Holesovsky <kendy at collabora.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 fda37f4b..9ea8fcc9 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 () {


More information about the Libreoffice-commits mailing list