17 |
ilm |
1 |
/*
|
|
|
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
|
|
|
3 |
*
|
|
|
4 |
* Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
|
|
|
5 |
*
|
|
|
6 |
* The contents of this file are subject to the terms of the GNU General Public License Version 3
|
|
|
7 |
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
|
|
|
8 |
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
|
|
|
9 |
* language governing permissions and limitations under the License.
|
|
|
10 |
*
|
|
|
11 |
* When distributing the software, include this License Header Notice in each file.
|
|
|
12 |
*/
|
|
|
13 |
|
|
|
14 |
package org.openconcerto.openoffice.spreadsheet;
|
|
|
15 |
|
80 |
ilm |
16 |
import org.openconcerto.openoffice.Length;
|
21 |
ilm |
17 |
import org.openconcerto.openoffice.LengthUnit;
|
17 |
ilm |
18 |
import org.openconcerto.openoffice.ODDocument;
|
20 |
ilm |
19 |
import org.openconcerto.openoffice.Style;
|
25 |
ilm |
20 |
import org.openconcerto.openoffice.StyleDesc;
|
20 |
ilm |
21 |
import org.openconcerto.openoffice.StyleStyleDesc;
|
73 |
ilm |
22 |
import org.openconcerto.openoffice.StyledNode;
|
17 |
ilm |
23 |
import org.openconcerto.openoffice.XMLVersion;
|
73 |
ilm |
24 |
import org.openconcerto.openoffice.spreadsheet.CellStyle.StyleTableCellProperties;
|
17 |
ilm |
25 |
import org.openconcerto.openoffice.spreadsheet.SheetTableModel.MutableTableModel;
|
|
|
26 |
import org.openconcerto.utils.CollectionUtils;
|
|
|
27 |
import org.openconcerto.utils.Tuple2;
|
|
|
28 |
import org.openconcerto.xml.JDOMUtils;
|
21 |
ilm |
29 |
import org.openconcerto.xml.SimpleXMLPath;
|
17 |
ilm |
30 |
|
|
|
31 |
import java.awt.Point;
|
|
|
32 |
import java.util.ArrayList;
|
21 |
ilm |
33 |
import java.util.HashMap;
|
17 |
ilm |
34 |
import java.util.HashSet;
|
|
|
35 |
import java.util.List;
|
21 |
ilm |
36 |
import java.util.Map;
|
17 |
ilm |
37 |
import java.util.Set;
|
|
|
38 |
import java.util.regex.Matcher;
|
|
|
39 |
|
|
|
40 |
import javax.swing.table.TableModel;
|
|
|
41 |
|
|
|
42 |
import org.jdom.Attribute;
|
|
|
43 |
import org.jdom.Element;
|
|
|
44 |
|
|
|
45 |
/**
|
|
|
46 |
* A single sheet in a spreadsheet.
|
|
|
47 |
*
|
|
|
48 |
* @author Sylvain
|
|
|
49 |
* @param <D> type of table parent
|
|
|
50 |
*/
|
|
|
51 |
public class Table<D extends ODDocument> extends TableCalcNode<TableStyle, D> {
|
|
|
52 |
|
|
|
53 |
static Element createEmpty(XMLVersion ns) {
|
65 |
ilm |
54 |
return createEmpty(ns, 1, 1);
|
|
|
55 |
}
|
|
|
56 |
|
|
|
57 |
static Element createEmpty(final XMLVersion ns, final int colCount, final int rowCount) {
|
73 |
ilm |
58 |
return createEmpty(ns, colCount, rowCount, null);
|
|
|
59 |
}
|
|
|
60 |
|
|
|
61 |
// pass styleName to avoid creating possibly expensive Table instance (readCols/readRows) in
|
|
|
62 |
// order to use setStyleName()
|
|
|
63 |
static Element createEmpty(final XMLVersion ns, final int colCount, final int rowCount, final String styleName) {
|
65 |
ilm |
64 |
// from the relaxNG
|
|
|
65 |
if (colCount < 1 || rowCount < 1)
|
|
|
66 |
throw new IllegalArgumentException("a table must have at least one cell");
|
17 |
ilm |
67 |
final Element col = Column.createEmpty(ns, null);
|
65 |
ilm |
68 |
Axis.COLUMN.setRepeated(col, colCount);
|
|
|
69 |
final Element row = Row.createEmpty(ns).addContent(Cell.createEmpty(ns, colCount));
|
|
|
70 |
Axis.ROW.setRepeated(row, rowCount);
|
73 |
ilm |
71 |
final Element res = new Element("table", ns.getTABLE()).addContent(col).addContent(row);
|
|
|
72 |
StyledNode.setStyleName(res, styleName);
|
|
|
73 |
return res;
|
17 |
ilm |
74 |
}
|
|
|
75 |
|
|
|
76 |
static final String getName(final Element elem) {
|
|
|
77 |
return elem.getAttributeValue("name", elem.getNamespace("table"));
|
|
|
78 |
}
|
|
|
79 |
|
|
|
80 |
// ATTN Row have their index as attribute
|
25 |
ilm |
81 |
private final ArrayList<Row<D>> rows;
|
|
|
82 |
private TableGroup rowGroup;
|
|
|
83 |
private final ArrayList<Column<D>> cols;
|
|
|
84 |
private TableGroup columnGroup;
|
17 |
ilm |
85 |
|
|
|
86 |
public Table(D parent, Element local) {
|
|
|
87 |
super(parent, local, TableStyle.class);
|
|
|
88 |
|
25 |
ilm |
89 |
this.rows = new ArrayList<Row<D>>(64);
|
|
|
90 |
this.cols = new ArrayList<Column<D>>(32);
|
17 |
ilm |
91 |
|
25 |
ilm |
92 |
// read columns first since Row constructor needs it
|
17 |
ilm |
93 |
this.readColumns();
|
|
|
94 |
this.readRows();
|
|
|
95 |
}
|
|
|
96 |
|
|
|
97 |
private void readColumns() {
|
25 |
ilm |
98 |
this.read(Axis.COLUMN);
|
17 |
ilm |
99 |
}
|
|
|
100 |
|
|
|
101 |
private final void readRows() {
|
25 |
ilm |
102 |
this.read(Axis.ROW);
|
17 |
ilm |
103 |
}
|
|
|
104 |
|
25 |
ilm |
105 |
private final void read(final Axis axis) {
|
|
|
106 |
final boolean col = axis == Axis.COLUMN;
|
|
|
107 |
final Tuple2<TableGroup, List<Element>> r = TableGroup.createRoot(this, axis);
|
|
|
108 |
final ArrayList<?> l = col ? this.cols : this.rows;
|
|
|
109 |
final int oldSize = l.size();
|
|
|
110 |
l.clear();
|
|
|
111 |
final int newSize = r.get0().getSize();
|
|
|
112 |
l.ensureCapacity(newSize);
|
|
|
113 |
if (col) {
|
|
|
114 |
final StyleStyleDesc<ColumnStyle> colStyleDesc = getColumnStyleDesc();
|
|
|
115 |
for (final Element clone : r.get1())
|
|
|
116 |
this.addCol(clone, colStyleDesc);
|
|
|
117 |
this.columnGroup = r.get0();
|
|
|
118 |
} else {
|
|
|
119 |
final StyleStyleDesc<RowStyle> rowStyleDesc = getRowStyleDesc();
|
|
|
120 |
final StyleStyleDesc<CellStyle> cellStyleDesc = getCellStyleDesc();
|
|
|
121 |
for (final Element clone : r.get1())
|
|
|
122 |
this.addRow(clone, rowStyleDesc, cellStyleDesc);
|
|
|
123 |
this.rowGroup = r.get0();
|
17 |
ilm |
124 |
}
|
25 |
ilm |
125 |
// this always copy the array, so make sure we reclaim enough memory (~ 64k)
|
|
|
126 |
if (oldSize - newSize > 8192) {
|
|
|
127 |
l.trimToSize();
|
|
|
128 |
}
|
|
|
129 |
assert newSize == (col ? this.getColumnCount() : this.getRowCount());
|
17 |
ilm |
130 |
}
|
|
|
131 |
|
25 |
ilm |
132 |
private final void addCol(Element clone, StyleStyleDesc<ColumnStyle> colStyleDesc) {
|
|
|
133 |
this.cols.add(new Column<D>(this, clone, colStyleDesc));
|
17 |
ilm |
134 |
}
|
|
|
135 |
|
25 |
ilm |
136 |
static final int flattenChildren(final List<Element> res, final Element elem, final Axis axis) {
|
|
|
137 |
int count = 0;
|
|
|
138 |
// array so that flatten1() can modify an int
|
|
|
139 |
int[] index = new int[] { 0 };
|
|
|
140 |
// copy since we will change our children (don't use List.listIterator(int) since it
|
|
|
141 |
// re-filters all content)
|
|
|
142 |
@SuppressWarnings("unchecked")
|
|
|
143 |
final List<Element> children = new ArrayList<Element>(elem.getChildren(axis.getElemName(), elem.getNamespace()));
|
|
|
144 |
final int stop = children.size();
|
|
|
145 |
for (int i = 0; i < stop; i++) {
|
|
|
146 |
final Element row = children.get(i);
|
|
|
147 |
count += flatten1(res, row, axis, index);
|
|
|
148 |
}
|
|
|
149 |
return count;
|
|
|
150 |
}
|
17 |
ilm |
151 |
|
25 |
ilm |
152 |
static int flatten1(final List<Element> res, final Element row, final Axis axis) {
|
|
|
153 |
return flatten1(res, row, axis, null);
|
17 |
ilm |
154 |
}
|
|
|
155 |
|
25 |
ilm |
156 |
// add XML elements to res and return the logical count
|
|
|
157 |
private static int flatten1(final List<Element> res, final Element row, final Axis axis, final int[] parentIndex) {
|
|
|
158 |
final int resSize = res.size();
|
|
|
159 |
final Attribute repeatedAttr = axis.getRepeatedAttr(row);
|
|
|
160 |
final int repeated = repeatedAttr == null ? 1 : Integer.parseInt(repeatedAttr.getValue());
|
|
|
161 |
if (axis == Axis.COLUMN && repeated > 1) {
|
|
|
162 |
row.removeAttribute(repeatedAttr);
|
|
|
163 |
final Element parent = row.getParentElement();
|
|
|
164 |
final int index = (parentIndex == null ? parent.indexOf(row) : parentIndex[0]) + 1;
|
|
|
165 |
res.add(row);
|
|
|
166 |
// -1 : we keep the original row
|
|
|
167 |
for (int i = 0; i < repeated - 1; i++) {
|
|
|
168 |
final Element clone = (Element) row.clone();
|
|
|
169 |
res.add(clone);
|
|
|
170 |
parent.addContent(index + i, clone);
|
17 |
ilm |
171 |
}
|
25 |
ilm |
172 |
} else {
|
|
|
173 |
res.add(row);
|
17 |
ilm |
174 |
}
|
25 |
ilm |
175 |
if (parentIndex != null)
|
|
|
176 |
parentIndex[0] += res.size() - resSize;
|
|
|
177 |
return repeated;
|
17 |
ilm |
178 |
}
|
|
|
179 |
|
|
|
180 |
public final String getName() {
|
|
|
181 |
return getName(this.getElement());
|
|
|
182 |
}
|
|
|
183 |
|
|
|
184 |
public final void setName(String name) {
|
|
|
185 |
this.getElement().setAttribute("name", name, this.getODDocument().getVersion().getTABLE());
|
|
|
186 |
}
|
|
|
187 |
|
|
|
188 |
public void detach() {
|
|
|
189 |
this.getElement().detach();
|
|
|
190 |
}
|
|
|
191 |
|
|
|
192 |
public final Object getPrintRanges() {
|
|
|
193 |
return this.getElement().getAttributeValue("print-ranges", this.getTABLE());
|
|
|
194 |
}
|
|
|
195 |
|
|
|
196 |
public final void setPrintRanges(String s) {
|
|
|
197 |
this.getElement().setAttribute("print-ranges", s, this.getTABLE());
|
|
|
198 |
}
|
|
|
199 |
|
|
|
200 |
public final void removePrintRanges() {
|
|
|
201 |
this.getElement().removeAttribute("print-ranges", this.getTABLE());
|
|
|
202 |
}
|
|
|
203 |
|
|
|
204 |
public final synchronized void duplicateFirstRows(int nbFirstRows, int nbDuplicate) {
|
|
|
205 |
this.duplicateRows(0, nbFirstRows, nbDuplicate);
|
|
|
206 |
}
|
|
|
207 |
|
|
|
208 |
public final synchronized void insertDuplicatedRows(int rowDuplicated, int nbDuplicate) {
|
|
|
209 |
this.duplicateRows(rowDuplicated, 1, nbDuplicate);
|
|
|
210 |
}
|
|
|
211 |
|
|
|
212 |
/**
|
|
|
213 |
* Clone a range of rows. Eg if you want to copy once rows 2 through 5, you call
|
|
|
214 |
* <code>duplicateRows(2, 4, 1)</code>.
|
|
|
215 |
*
|
|
|
216 |
* @param start the first row to clone.
|
80 |
ilm |
217 |
* @param count the number of rows after <code>start</code> to clone, i.e. 1 means clone one
|
|
|
218 |
* row, must be greater or equal to 0.
|
|
|
219 |
* @param copies the number of copies of the range to make, must be greater or equal to 0.
|
|
|
220 |
* @throws IllegalArgumentException if arguments are not valid.
|
17 |
ilm |
221 |
*/
|
80 |
ilm |
222 |
public final void duplicateRows(final int start, final int count, final int copies) throws IllegalArgumentException {
|
21 |
ilm |
223 |
this.duplicateRows(start, count, copies, true);
|
|
|
224 |
}
|
19 |
ilm |
225 |
|
80 |
ilm |
226 |
public final synchronized void duplicateRows(final int start, final int count, final int copies, final boolean updateCellAddresses) throws IllegalArgumentException {
|
|
|
227 |
if (start < 0)
|
|
|
228 |
throw new IllegalArgumentException("Negative start index : " + start);
|
|
|
229 |
if (count < 0)
|
|
|
230 |
throw new IllegalArgumentException("Negative count : " + count);
|
|
|
231 |
else if (count == 0)
|
|
|
232 |
return;
|
|
|
233 |
if (copies < 0)
|
|
|
234 |
throw new IllegalArgumentException("Negative copies : " + copies);
|
|
|
235 |
else if (copies == 0)
|
|
|
236 |
return;
|
|
|
237 |
// stop is excluded
|
17 |
ilm |
238 |
final int stop = start + count;
|
80 |
ilm |
239 |
if (stop > this.getRowCount())
|
|
|
240 |
throw new IllegalArgumentException("Last row to duplicate (" + (stop - 1) + ") doesn't exist, there's only : " + getRowCount());
|
|
|
241 |
|
21 |
ilm |
242 |
// should not change merged status
|
|
|
243 |
final Map<Point, Integer> coverOrigins = new HashMap<Point, Integer>();
|
|
|
244 |
final List<Point> coverOriginsToUpdate = new ArrayList<Point>();
|
|
|
245 |
final int colCount = this.getColumnCount();
|
|
|
246 |
for (int x = 0; x < colCount;) {
|
|
|
247 |
int y = start;
|
|
|
248 |
while (y < stop) {
|
|
|
249 |
final Point coverOrigin = this.getCoverOrigin(x, y);
|
|
|
250 |
if (coverOrigin == null) {
|
|
|
251 |
y++;
|
|
|
252 |
} else {
|
|
|
253 |
final int lastCoveredCellRow;
|
|
|
254 |
// if we have already encountered this merged cell, skip it
|
|
|
255 |
if (coverOrigins.containsKey(coverOrigin)) {
|
|
|
256 |
lastCoveredCellRow = coverOrigins.get(coverOrigin);
|
|
|
257 |
} else {
|
|
|
258 |
final Cell<D> covering = this.getImmutableCellAt(coverOrigin.x, coverOrigin.y);
|
|
|
259 |
lastCoveredCellRow = coverOrigin.y + covering.getRowsSpanned() - 1;
|
|
|
260 |
if (coverOrigin.y < start) {
|
|
|
261 |
if (lastCoveredCellRow < stop - 1)
|
|
|
262 |
throw new IllegalArgumentException("Span starts before the duplicated rows and doesn't extend past the end of duplicated rows at " + getAddress(coverOrigin));
|
|
|
263 |
} else {
|
|
|
264 |
if (lastCoveredCellRow > stop - 1)
|
|
|
265 |
throw new IllegalArgumentException("Span starts in the duplicated rows and extend past the end of duplicated rows at " + getAddress(coverOrigin));
|
|
|
266 |
}
|
|
|
267 |
|
|
|
268 |
coverOrigins.put(coverOrigin, lastCoveredCellRow);
|
|
|
269 |
// merged cells inside the duplicated rows don't need to be updated
|
|
|
270 |
if (coverOrigin.y < start || lastCoveredCellRow > stop - 1)
|
|
|
271 |
coverOriginsToUpdate.add(coverOrigin);
|
|
|
272 |
}
|
|
|
273 |
y = lastCoveredCellRow + 1;
|
|
|
274 |
}
|
|
|
275 |
}
|
|
|
276 |
x++;
|
|
|
277 |
}
|
|
|
278 |
|
17 |
ilm |
279 |
// clone xml elements and add them to our tree
|
|
|
280 |
final List<Element> clones = new ArrayList<Element>(count * copies);
|
25 |
ilm |
281 |
for (int l = start; l < stop;) {
|
|
|
282 |
final Row<D> immutableRow = this.getRow(l);
|
|
|
283 |
final Row<D> toClone;
|
|
|
284 |
// MAYBE use something else than getMutableRow() since we don't need a single row.
|
|
|
285 |
// the repeated row starts before the copied range, split it at the beginning
|
|
|
286 |
if (immutableRow.getY() < l) {
|
|
|
287 |
toClone = this.getMutableRow(l);
|
|
|
288 |
} else {
|
|
|
289 |
assert immutableRow.getY() == l;
|
|
|
290 |
if (immutableRow.getLastY() >= stop) {
|
|
|
291 |
// the repeated row goes beyond the copied range, split it at the end
|
|
|
292 |
assert this.getRow(stop) == immutableRow;
|
|
|
293 |
this.getMutableRow(stop);
|
|
|
294 |
toClone = this.getRow(l);
|
|
|
295 |
} else {
|
|
|
296 |
toClone = immutableRow;
|
|
|
297 |
}
|
17 |
ilm |
298 |
}
|
25 |
ilm |
299 |
assert toClone.getY() == l;
|
|
|
300 |
assert toClone.getLastY() < stop : "Row goes to far";
|
|
|
301 |
l += toClone.getRepeated();
|
|
|
302 |
clones.add((Element) toClone.getElement().clone());
|
17 |
ilm |
303 |
}
|
25 |
ilm |
304 |
final int clonesSize = clones.size();
|
|
|
305 |
for (int i = 1; i < copies; i++) {
|
|
|
306 |
for (int j = 0; j < clonesSize; j++) {
|
|
|
307 |
clones.add((Element) clones.get(j).clone());
|
|
|
308 |
}
|
|
|
309 |
}
|
17 |
ilm |
310 |
// works anywhere its XML element is
|
25 |
ilm |
311 |
assert this.getRow(stop - 1).getLastY() == stop - 1 : "Adding XML element too far";
|
|
|
312 |
JDOMUtils.insertAfter(this.getRow(stop - 1).getElement(), clones);
|
17 |
ilm |
313 |
|
21 |
ilm |
314 |
for (final Point coverOrigin : coverOriginsToUpdate) {
|
|
|
315 |
final MutableCell<D> coveringCell = getCellAt(coverOrigin);
|
|
|
316 |
coveringCell.setRowsSpanned(coveringCell.getRowsSpanned() + count * copies);
|
|
|
317 |
}
|
|
|
318 |
|
25 |
ilm |
319 |
// synchronize our rows with our new tree (rows' index have changed)
|
17 |
ilm |
320 |
this.readRows();
|
19 |
ilm |
321 |
|
21 |
ilm |
322 |
// 19.627 in OpenDocument-v1.2-cs01-part1 : The table:end-cell-address attribute specifies
|
|
|
323 |
// end position of the shape if it is included in a spreadsheet document.
|
|
|
324 |
if (updateCellAddresses && getODDocument() instanceof SpreadSheet) {
|
|
|
325 |
final SpreadSheet ssheet = (SpreadSheet) getODDocument();
|
|
|
326 |
final SimpleXMLPath<Attribute> descAttrs = SimpleXMLPath.allAttributes("end-cell-address", "table");
|
|
|
327 |
for (final Attribute endCellAttr : descAttrs.selectNodes(getElement())) {
|
|
|
328 |
final Tuple2<Sheet, Point> resolved = ssheet.resolve(endCellAttr.getValue());
|
|
|
329 |
final Sheet endCellSheet = resolved.get0();
|
|
|
330 |
if (endCellSheet != this)
|
|
|
331 |
throw new UnsupportedOperationException("End sheet is not this : " + endCellSheet);
|
|
|
332 |
final Point endCellPoint = resolved.get1();
|
|
|
333 |
// if the end point is before the copied rows, nothing to do
|
|
|
334 |
if (endCellPoint.y >= start) {
|
|
|
335 |
final Element endCellParentElem = endCellAttr.getParent();
|
19 |
ilm |
336 |
|
21 |
ilm |
337 |
// find row index of the shape
|
|
|
338 |
final Element rowElem = JDOMUtils.getAncestor(endCellParentElem, "table-row", getTABLE());
|
|
|
339 |
if (rowElem == null)
|
|
|
340 |
throw new IllegalStateException("Not in a row : " + JDOMUtils.output(endCellParentElem));
|
|
|
341 |
int startRowIndex = -1;
|
|
|
342 |
final int rowCount = getRowCount();
|
|
|
343 |
for (int i = 0; i < rowCount; i++) {
|
|
|
344 |
if (getRow(i).getElement() == rowElem) {
|
|
|
345 |
startRowIndex = i;
|
|
|
346 |
break;
|
|
|
347 |
}
|
|
|
348 |
}
|
|
|
349 |
if (startRowIndex < 0)
|
|
|
350 |
throw new IllegalStateException("Row not found for " + JDOMUtils.output(endCellParentElem));
|
|
|
351 |
final int newEndY;
|
|
|
352 |
if (startRowIndex >= start + (copies + 1) * count) {
|
|
|
353 |
// if the shape doesn't span over the copied rows, only need to offset
|
|
|
354 |
// end-cell-address
|
|
|
355 |
newEndY = endCellPoint.y + copies * count;
|
|
|
356 |
} else if (startRowIndex >= start + count && endCellPoint.y < start + count) {
|
|
|
357 |
// if the shape was copied and its end cell too, translate it
|
|
|
358 |
// find in which copy the shape is in, ATTN the truncation is important
|
|
|
359 |
// since the shape might not be in the first copied row
|
|
|
360 |
final int nth = (startRowIndex - start) / count;
|
|
|
361 |
newEndY = endCellPoint.y + nth * count;
|
|
|
362 |
} else {
|
|
|
363 |
// we must use height to compute new values for end-cell-address and end-y
|
19 |
ilm |
364 |
|
21 |
ilm |
365 |
// find the height of the shape
|
93 |
ilm |
366 |
final Length[] coordinates = getODDocument().getFormatVersion().getXML().getCoordinates(endCellParentElem, false, true);
|
21 |
ilm |
367 |
if (coordinates == null)
|
|
|
368 |
throw new IllegalStateException("Couldn't find the height of the shape : " + JDOMUtils.output(endCellParentElem));
|
93 |
ilm |
369 |
final Length endYFromAnchor = coordinates[3];
|
21 |
ilm |
370 |
assert endYFromAnchor != null : "getCoordinates() should never return null BigDecimal (unless requested by horizontal/vertical)";
|
|
|
371 |
// find the end row
|
|
|
372 |
int rowIndex = startRowIndex;
|
93 |
ilm |
373 |
Length cellEndYFromAnchor = getRow(rowIndex).getStyle().getTableRowProperties().getHeight();
|
21 |
ilm |
374 |
while (endYFromAnchor.compareTo(cellEndYFromAnchor) > 0) {
|
|
|
375 |
rowIndex++;
|
93 |
ilm |
376 |
cellEndYFromAnchor = cellEndYFromAnchor.add(getRow(rowIndex).getStyle().getTableRowProperties().getHeight());
|
21 |
ilm |
377 |
}
|
|
|
378 |
// find the end-y
|
93 |
ilm |
379 |
final Length cellStartYFromAnchor = cellEndYFromAnchor.subtract(getRow(rowIndex).getStyle().getTableRowProperties().getHeight());
|
|
|
380 |
final Length endY = endYFromAnchor.subtract(cellStartYFromAnchor);
|
21 |
ilm |
381 |
assert endY.signum() >= 0;
|
|
|
382 |
|
|
|
383 |
newEndY = rowIndex;
|
93 |
ilm |
384 |
endCellParentElem.setAttribute("end-y", endY.format(), getTABLE());
|
19 |
ilm |
385 |
}
|
21 |
ilm |
386 |
endCellAttr.setValue(SpreadSheet.formatSheetName(endCellSheet.getName()) + "." + Table.getAddress(new Point(endCellPoint.x, newEndY)));
|
19 |
ilm |
387 |
}
|
|
|
388 |
}
|
|
|
389 |
}
|
17 |
ilm |
390 |
}
|
|
|
391 |
|
25 |
ilm |
392 |
private synchronized void addRow(Element child, StyleDesc<RowStyle> styleDesc, StyleDesc<CellStyle> cellStyleDesc) {
|
|
|
393 |
final Row<D> row = new Row<D>(this, child, this.rows.size(), styleDesc, cellStyleDesc);
|
|
|
394 |
final int toRepeat = row.getRepeated();
|
|
|
395 |
for (int i = 0; i < toRepeat; i++) {
|
|
|
396 |
this.rows.add(row);
|
|
|
397 |
}
|
17 |
ilm |
398 |
}
|
|
|
399 |
|
|
|
400 |
public final Point resolveHint(String ref) {
|
|
|
401 |
final Point res = resolve(ref);
|
|
|
402 |
if (res != null) {
|
|
|
403 |
return res;
|
|
|
404 |
} else
|
|
|
405 |
throw new IllegalArgumentException(ref + " is not a cell ref, if it's a named range, you must use it on a SpreadSheet.");
|
|
|
406 |
}
|
|
|
407 |
|
|
|
408 |
// *** set cell
|
|
|
409 |
|
|
|
410 |
public final boolean isCellValid(int x, int y) {
|
|
|
411 |
if (x > this.getColumnCount())
|
|
|
412 |
return false;
|
|
|
413 |
else if (y > this.getRowCount())
|
|
|
414 |
return false;
|
|
|
415 |
else
|
|
|
416 |
return this.getImmutableCellAt(x, y).isValid();
|
|
|
417 |
}
|
|
|
418 |
|
25 |
ilm |
419 |
/**
|
|
|
420 |
* Return a modifiable cell at the passed coordinates. This is slower than
|
|
|
421 |
* {@link #getImmutableCellAt(int, int)} since this method may modify the underlying XML (e.g.
|
|
|
422 |
* break up repeated cells to allow for modification of only the returned cell).
|
|
|
423 |
*
|
|
|
424 |
* @param x the column.
|
|
|
425 |
* @param y the row.
|
|
|
426 |
* @return the cell.
|
|
|
427 |
* @see #getImmutableCellAt(int, int)
|
|
|
428 |
*/
|
17 |
ilm |
429 |
public final MutableCell<D> getCellAt(int x, int y) {
|
156 |
ilm |
430 |
final Row<D> r = this.getMutableRow(y);
|
|
|
431 |
try {
|
|
|
432 |
return r.getMutableCellAt(x);
|
|
|
433 |
} catch (Exception e) {
|
|
|
434 |
throw new IllegalArgumentException("Couldn't get mutable cell at " + getAddress(x, y), e);
|
|
|
435 |
}
|
17 |
ilm |
436 |
}
|
|
|
437 |
|
|
|
438 |
public final MutableCell<D> getCellAt(String ref) {
|
|
|
439 |
return this.getCellAt(resolveHint(ref));
|
|
|
440 |
}
|
|
|
441 |
|
|
|
442 |
final MutableCell<D> getCellAt(Point p) {
|
|
|
443 |
return this.getCellAt(p.x, p.y);
|
|
|
444 |
}
|
|
|
445 |
|
|
|
446 |
/**
|
|
|
447 |
* Sets the value at the specified coordinates.
|
|
|
448 |
*
|
|
|
449 |
* @param val the new value, <code>null</code> will be treated as "".
|
|
|
450 |
* @param x the column.
|
|
|
451 |
* @param y the row.
|
|
|
452 |
*/
|
|
|
453 |
public final void setValueAt(Object val, int x, int y) {
|
|
|
454 |
if (val == null)
|
|
|
455 |
val = "";
|
|
|
456 |
// ne pas casser les repeated pour rien
|
|
|
457 |
if (!val.equals(this.getValueAt(x, y)))
|
|
|
458 |
this.getCellAt(x, y).setValue(val);
|
|
|
459 |
}
|
|
|
460 |
|
|
|
461 |
// *** get cell
|
|
|
462 |
|
25 |
ilm |
463 |
/**
|
|
|
464 |
* Return a non modifiable cell at the passed coordinates. This is faster than
|
|
|
465 |
* {@link #getCellAt(int, int)} since this method never modifies the underlying XML.
|
|
|
466 |
*
|
|
|
467 |
* @param x the column.
|
|
|
468 |
* @param y the row.
|
|
|
469 |
* @return the cell.
|
|
|
470 |
* @see #getCellAt(int, int)
|
|
|
471 |
*/
|
|
|
472 |
public final Cell<D> getImmutableCellAt(int x, int y) {
|
17 |
ilm |
473 |
return this.getRow(y).getCellAt(x);
|
|
|
474 |
}
|
|
|
475 |
|
25 |
ilm |
476 |
public final Cell<D> getImmutableCellAt(String ref) {
|
17 |
ilm |
477 |
final Point p = resolveHint(ref);
|
|
|
478 |
return this.getImmutableCellAt(p.x, p.y);
|
|
|
479 |
}
|
|
|
480 |
|
|
|
481 |
/**
|
21 |
ilm |
482 |
* Return the origin of a merged cell.
|
|
|
483 |
*
|
|
|
484 |
* @param x the column.
|
|
|
485 |
* @param y the row.
|
|
|
486 |
* @return the point of origin, <code>null</code> if there's no merged cell at the passed
|
|
|
487 |
* coordinates.
|
|
|
488 |
*/
|
|
|
489 |
public final Point getCoverOrigin(final int x, final int y) {
|
|
|
490 |
// can't return a Cell, since it has no x
|
|
|
491 |
// don't return a MutableCell since it is costly
|
|
|
492 |
|
|
|
493 |
final Cell<D> c = this.getImmutableCellAt(x, y);
|
|
|
494 |
if (c.coversOtherCells()) {
|
|
|
495 |
return new Point(x, y);
|
|
|
496 |
} else if (!c.isCovered()) {
|
|
|
497 |
return null;
|
|
|
498 |
} else {
|
|
|
499 |
final Row<D> row = this.getRow(y);
|
|
|
500 |
Cell<D> currentCell = c;
|
|
|
501 |
int currentX = x;
|
|
|
502 |
while (currentX > 0 && currentCell.isCovered()) {
|
|
|
503 |
currentX--;
|
|
|
504 |
currentCell = row.getCellAt(currentX);
|
|
|
505 |
}
|
|
|
506 |
if (currentCell.coversOtherCells())
|
|
|
507 |
return new Point(currentX, y);
|
|
|
508 |
|
|
|
509 |
if (!currentCell.isCovered()) {
|
|
|
510 |
currentX++;
|
|
|
511 |
currentCell = row.getCellAt(currentX);
|
|
|
512 |
}
|
|
|
513 |
assert currentCell.isCovered();
|
|
|
514 |
|
|
|
515 |
int currentY = y;
|
|
|
516 |
while (!currentCell.coversOtherCells()) {
|
|
|
517 |
currentY--;
|
|
|
518 |
currentCell = this.getImmutableCellAt(currentX, currentY);
|
|
|
519 |
}
|
|
|
520 |
return new Point(currentX, currentY);
|
|
|
521 |
}
|
|
|
522 |
}
|
|
|
523 |
|
|
|
524 |
/**
|
80 |
ilm |
525 |
* The value of the cell at the passed coordinates.
|
|
|
526 |
*
|
|
|
527 |
* @param row row index from <code>0</code> to <code>{@link #getRowCount() row count}-1</code>
|
|
|
528 |
* @param column column index from <code>0</code> to
|
|
|
529 |
* <code>{@link #getColumnCount() column count}-1</code>
|
|
|
530 |
* @return the value at the passed coordinates.
|
|
|
531 |
* @see #getValueAt(String)
|
|
|
532 |
* @see Cell#getValue()
|
17 |
ilm |
533 |
*/
|
|
|
534 |
public final Object getValueAt(int column, int row) {
|
|
|
535 |
return this.getImmutableCellAt(column, row).getValue();
|
|
|
536 |
}
|
|
|
537 |
|
|
|
538 |
/**
|
|
|
539 |
* Find the style name for the specified cell.
|
|
|
540 |
*
|
|
|
541 |
* @param column column index.
|
|
|
542 |
* @param row row index.
|
|
|
543 |
* @return the style name, can be <code>null</code>.
|
|
|
544 |
*/
|
|
|
545 |
public final String getStyleNameAt(int column, int row) {
|
|
|
546 |
// first the cell
|
|
|
547 |
String cellStyle = this.getImmutableCellAt(column, row).getStyleAttr();
|
|
|
548 |
if (cellStyle != null)
|
|
|
549 |
return cellStyle;
|
|
|
550 |
// then the row (as specified in §2 of section 8.1)
|
73 |
ilm |
551 |
// in reality LO never generates table-row/@default-cell-style-name and its behavior is
|
|
|
552 |
// really unpredictable if it opens files with it
|
67 |
ilm |
553 |
if (Style.isStandardStyleResolution()) {
|
|
|
554 |
cellStyle = this.getRow(row).getElement().getAttributeValue("default-cell-style-name", getTABLE());
|
|
|
555 |
if (cellStyle != null)
|
|
|
556 |
return cellStyle;
|
|
|
557 |
}
|
17 |
ilm |
558 |
// and finally the column
|
|
|
559 |
return this.getColumn(column).getElement().getAttributeValue("default-cell-style-name", getTABLE());
|
|
|
560 |
}
|
|
|
561 |
|
|
|
562 |
public final CellStyle getStyleAt(int column, int row) {
|
25 |
ilm |
563 |
return getCellStyleDesc().findStyleForNode(this.getImmutableCellAt(column, row), this.getStyleNameAt(column, row));
|
17 |
ilm |
564 |
}
|
|
|
565 |
|
73 |
ilm |
566 |
public final StyleTableCellProperties getTableCellPropertiesAt(int column, int row) {
|
|
|
567 |
final CellStyle styleAt = this.getStyleAt(column, row);
|
|
|
568 |
return styleAt == null ? null : styleAt.getTableCellProperties(this.getImmutableCellAt(column, row));
|
|
|
569 |
}
|
|
|
570 |
|
20 |
ilm |
571 |
protected StyleStyleDesc<CellStyle> getCellStyleDesc() {
|
|
|
572 |
return Style.getStyleStyleDesc(CellStyle.class, getODDocument().getVersion());
|
|
|
573 |
}
|
|
|
574 |
|
|
|
575 |
public final CellStyle getDefaultCellStyle() {
|
|
|
576 |
return getCellStyleDesc().findDefaultStyle(this.getODDocument().getPackage());
|
|
|
577 |
}
|
|
|
578 |
|
17 |
ilm |
579 |
/**
|
|
|
580 |
* Return the coordinates of cells using the passed style.
|
|
|
581 |
*
|
|
|
582 |
* @param cellStyleName a style name.
|
|
|
583 |
* @return the cells using <code>cellStyleName</code>.
|
|
|
584 |
*/
|
|
|
585 |
public final List<Tuple2<Integer, Integer>> getStyleReferences(final String cellStyleName) {
|
|
|
586 |
final List<Tuple2<Integer, Integer>> res = new ArrayList<Tuple2<Integer, Integer>>();
|
|
|
587 |
final Set<Integer> cols = new HashSet<Integer>();
|
|
|
588 |
final int columnCount = getColumnCount();
|
|
|
589 |
for (int i = 0; i < columnCount; i++) {
|
|
|
590 |
if (cellStyleName.equals(this.getColumn(i).getElement().getAttributeValue("default-cell-style-name", getTABLE())))
|
|
|
591 |
cols.add(i);
|
|
|
592 |
}
|
|
|
593 |
final int rowCount = getRowCount();
|
|
|
594 |
for (int y = 0; y < rowCount; y++) {
|
|
|
595 |
final Row<D> row = this.getRow(y);
|
|
|
596 |
final String rowStyle = row.getElement().getAttributeValue("default-cell-style-name", getTABLE());
|
|
|
597 |
for (int x = 0; x < columnCount; x++) {
|
|
|
598 |
final String cellStyle = row.getCellAt(x).getStyleAttr();
|
|
|
599 |
final boolean match;
|
|
|
600 |
// first the cell
|
|
|
601 |
if (cellStyle != null)
|
|
|
602 |
match = cellStyleName.equals(cellStyle);
|
|
|
603 |
// then the row (as specified in §2 of section 8.1)
|
|
|
604 |
else if (rowStyle != null)
|
|
|
605 |
match = cellStyleName.equals(rowStyle);
|
|
|
606 |
// and finally the column
|
|
|
607 |
else
|
|
|
608 |
match = cols.contains(x);
|
|
|
609 |
|
|
|
610 |
if (match)
|
|
|
611 |
res.add(Tuple2.create(x, y));
|
|
|
612 |
}
|
|
|
613 |
}
|
|
|
614 |
return res;
|
|
|
615 |
}
|
|
|
616 |
|
25 |
ilm |
617 |
protected final StyleStyleDesc<ColumnStyle> getColumnStyleDesc() {
|
|
|
618 |
return Style.getStyleStyleDesc(ColumnStyle.class, XMLVersion.getVersion(getElement()));
|
|
|
619 |
}
|
|
|
620 |
|
|
|
621 |
protected final StyleStyleDesc<RowStyle> getRowStyleDesc() {
|
|
|
622 |
return Style.getStyleStyleDesc(RowStyle.class, XMLVersion.getVersion(getElement()));
|
|
|
623 |
}
|
|
|
624 |
|
17 |
ilm |
625 |
/**
|
80 |
ilm |
626 |
* The value of the cell at the passed coordinates.
|
17 |
ilm |
627 |
*
|
80 |
ilm |
628 |
* @param ref a reference, e.g. "A3".
|
|
|
629 |
* @return the value at the passed coordinates.
|
|
|
630 |
* @see #getValueAt(int, int)
|
|
|
631 |
* @see Cell#getValue()
|
17 |
ilm |
632 |
*/
|
|
|
633 |
public final Object getValueAt(String ref) {
|
|
|
634 |
return this.getImmutableCellAt(ref).getValue();
|
|
|
635 |
}
|
|
|
636 |
|
|
|
637 |
// *** get count
|
|
|
638 |
|
93 |
ilm |
639 |
// not public since Row represent the physical (XML) row, i.e. it can represent many logical
|
|
|
640 |
// rows. Ideally two classes should be created (as Cell/MutableCell), but this would only be
|
|
|
641 |
// useful for changing style (e.g. setHeight()).
|
|
|
642 |
|
25 |
ilm |
643 |
final Row<D> getRow(int index) {
|
17 |
ilm |
644 |
return this.rows.get(index);
|
|
|
645 |
}
|
|
|
646 |
|
25 |
ilm |
647 |
final Row<D> getMutableRow(int y) {
|
|
|
648 |
final Row<D> c = this.getRow(y);
|
|
|
649 |
if (c.getRepeated() > 1) {
|
|
|
650 |
RepeatedBreaker.<D> getRowBreaker().breakRepeated(this, this.rows, y);
|
|
|
651 |
return this.getRow(y);
|
|
|
652 |
} else {
|
|
|
653 |
return c;
|
|
|
654 |
}
|
|
|
655 |
}
|
|
|
656 |
|
93 |
ilm |
657 |
// OK to use immutable row since for now RowStyle has no setter (except for raw methods in super
|
|
|
658 |
// classes)
|
|
|
659 |
public final RowStyle getRowStyle(int index) {
|
|
|
660 |
return this.getRow(index).getStyle();
|
|
|
661 |
}
|
|
|
662 |
|
17 |
ilm |
663 |
public final Column<D> getColumn(int i) {
|
|
|
664 |
return this.cols.get(i);
|
|
|
665 |
}
|
|
|
666 |
|
93 |
ilm |
667 |
public final ColumnStyle getColumnStyle(int index) {
|
|
|
668 |
return this.getColumn(index).getStyle();
|
|
|
669 |
}
|
|
|
670 |
|
17 |
ilm |
671 |
public final int getRowCount() {
|
|
|
672 |
return this.rows.size();
|
|
|
673 |
}
|
|
|
674 |
|
25 |
ilm |
675 |
public final TableGroup getRowGroup() {
|
|
|
676 |
return this.rowGroup;
|
|
|
677 |
}
|
|
|
678 |
|
|
|
679 |
/**
|
|
|
680 |
* Return the deepest group at the passed row.
|
|
|
681 |
*
|
|
|
682 |
* @param y a row index.
|
|
|
683 |
* @return the group at the index, never <code>null</code>.
|
|
|
684 |
*/
|
|
|
685 |
public final TableGroup getRowGroupAt(final int y) {
|
|
|
686 |
return this.getRowGroup().getDescendentOrSelfContaining(y);
|
|
|
687 |
}
|
|
|
688 |
|
17 |
ilm |
689 |
public final int getHeaderRowCount() {
|
25 |
ilm |
690 |
return this.getRowGroup().getFollowingHeaderCount();
|
17 |
ilm |
691 |
}
|
|
|
692 |
|
|
|
693 |
public final int getColumnCount() {
|
|
|
694 |
return this.cols.size();
|
|
|
695 |
}
|
|
|
696 |
|
25 |
ilm |
697 |
public final TableGroup getColumnGroup() {
|
|
|
698 |
return this.columnGroup;
|
|
|
699 |
}
|
|
|
700 |
|
|
|
701 |
/**
|
|
|
702 |
* Return the deepest group at the passed column.
|
|
|
703 |
*
|
|
|
704 |
* @param x a column index.
|
|
|
705 |
* @return the group at the index, never <code>null</code>.
|
|
|
706 |
*/
|
|
|
707 |
public final TableGroup getColumnGroupAt(final int x) {
|
|
|
708 |
return this.getColumnGroup().getDescendentOrSelfContaining(x);
|
|
|
709 |
}
|
|
|
710 |
|
17 |
ilm |
711 |
public final int getHeaderColumnCount() {
|
25 |
ilm |
712 |
return this.getColumnGroup().getFollowingHeaderCount();
|
17 |
ilm |
713 |
}
|
|
|
714 |
|
|
|
715 |
// *** set count
|
|
|
716 |
|
|
|
717 |
/**
|
|
|
718 |
* Changes the column count without keeping the table width.
|
|
|
719 |
*
|
|
|
720 |
* @param newSize the new column count.
|
|
|
721 |
* @see #setColumnCount(int, int, boolean)
|
|
|
722 |
*/
|
|
|
723 |
public final void setColumnCount(int newSize) {
|
|
|
724 |
this.setColumnCount(newSize, -1, false);
|
|
|
725 |
}
|
|
|
726 |
|
|
|
727 |
/**
|
|
|
728 |
* Assure that this sheet has at least <code>newSize</code> columns.
|
|
|
729 |
*
|
|
|
730 |
* @param newSize the minimum column count this table should have.
|
|
|
731 |
*/
|
|
|
732 |
public final void ensureColumnCount(int newSize) {
|
|
|
733 |
if (newSize > this.getColumnCount())
|
|
|
734 |
this.setColumnCount(newSize);
|
|
|
735 |
}
|
|
|
736 |
|
|
|
737 |
/**
|
|
|
738 |
* Changes the column count. If <code>newSize</code> is less than {@link #getColumnCount()}
|
|
|
739 |
* extra cells will be chopped off. Otherwise empty cells will be created.
|
|
|
740 |
*
|
|
|
741 |
* @param newSize the new column count.
|
73 |
ilm |
742 |
* @param colIndex the index of the column to be copied, -1 for empty column (and no style).
|
17 |
ilm |
743 |
* @param keepTableWidth <code>true</code> if the table should be same width after the column
|
|
|
744 |
* change.
|
|
|
745 |
*/
|
|
|
746 |
public final void setColumnCount(int newSize, int colIndex, final boolean keepTableWidth) {
|
73 |
ilm |
747 |
this.setColumnCount(newSize, colIndex, null, keepTableWidth);
|
|
|
748 |
}
|
|
|
749 |
|
|
|
750 |
/**
|
|
|
751 |
* Changes the column count. If <code>newSize</code> is less than {@link #getColumnCount()}
|
|
|
752 |
* extra cells will be chopped off. Otherwise empty cells will be created.
|
|
|
753 |
*
|
|
|
754 |
* @param newSize the new column count.
|
|
|
755 |
* @param colStyle the style of the new columns, <code>null</code> for no style (throws
|
|
|
756 |
* exception if the table has a non-null width).
|
|
|
757 |
* @param keepTableWidth <code>true</code> if the table should be same width after the column
|
|
|
758 |
* change.
|
|
|
759 |
* @see #createColumnStyle(Number, LengthUnit)
|
|
|
760 |
*/
|
|
|
761 |
public final void setColumnCount(int newSize, ColumnStyle colStyle, final boolean keepTableWidth) {
|
|
|
762 |
this.setColumnCount(newSize, -1, colStyle, keepTableWidth);
|
|
|
763 |
}
|
|
|
764 |
|
|
|
765 |
private final void setColumnCount(int newSize, int colIndex, ColumnStyle colStyle, final boolean keepTableWidth) {
|
17 |
ilm |
766 |
final int toGrow = newSize - this.getColumnCount();
|
|
|
767 |
if (toGrow < 0) {
|
|
|
768 |
this.removeColumn(newSize, this.getColumnCount(), keepTableWidth);
|
|
|
769 |
} else if (toGrow > 0) {
|
|
|
770 |
// the list of columns cannot be mixed with other elements
|
|
|
771 |
// so just keep adding after the last one
|
|
|
772 |
final int indexOfLastCol;
|
|
|
773 |
if (this.getColumnCount() == 0)
|
|
|
774 |
// from section 8.1.1 the only possible elements after cols are rows
|
|
|
775 |
// but there can't be rows w/o columns, so just add to the end
|
|
|
776 |
indexOfLastCol = this.getElement().getContentSize() - 1;
|
|
|
777 |
else
|
|
|
778 |
indexOfLastCol = this.getElement().getContent().indexOf(this.getColumn(this.getColumnCount() - 1).getElement());
|
|
|
779 |
|
|
|
780 |
final Element elemToClone;
|
|
|
781 |
if (colIndex < 0) {
|
73 |
ilm |
782 |
elemToClone = Column.createEmpty(getODDocument().getVersion(), colStyle);
|
17 |
ilm |
783 |
} else {
|
|
|
784 |
elemToClone = getColumn(colIndex).getElement();
|
|
|
785 |
}
|
25 |
ilm |
786 |
final StyleStyleDesc<ColumnStyle> columnStyleDesc = getColumnStyleDesc();
|
17 |
ilm |
787 |
for (int i = 0; i < toGrow; i++) {
|
|
|
788 |
final Element newElem = (Element) elemToClone.clone();
|
|
|
789 |
this.getElement().addContent(indexOfLastCol + 1 + i, newElem);
|
25 |
ilm |
790 |
this.cols.add(new Column<D>(this, newElem, columnStyleDesc));
|
17 |
ilm |
791 |
}
|
|
|
792 |
// now update widths
|
|
|
793 |
updateWidth(keepTableWidth);
|
|
|
794 |
|
|
|
795 |
// add needed cells
|
25 |
ilm |
796 |
final StyleStyleDesc<CellStyle> cellStyleDesc = this.getCellStyleDesc();
|
|
|
797 |
final int rowCount = this.getRowCount();
|
|
|
798 |
for (int i = 0; i < rowCount;) {
|
|
|
799 |
final Row<D> r = this.getRow(i);
|
|
|
800 |
r.columnCountChanged(cellStyleDesc);
|
|
|
801 |
i += r.getRepeated();
|
17 |
ilm |
802 |
}
|
|
|
803 |
}
|
|
|
804 |
}
|
|
|
805 |
|
|
|
806 |
public final void removeColumn(int colIndex, final boolean keepTableWidth) {
|
|
|
807 |
this.removeColumn(colIndex, colIndex + 1, keepTableWidth);
|
|
|
808 |
}
|
|
|
809 |
|
|
|
810 |
/**
|
|
|
811 |
* Remove columns from this. As with OpenOffice, no cell must be covered in the column to
|
|
|
812 |
* remove. ATTN <code>keepTableWidth</code> only works for tables in text document that are not
|
|
|
813 |
* aligned automatically (ie fill the entire page). ATTN spreadsheet applications may hide from
|
|
|
814 |
* you the real width of sheets, eg display only columns A to AJ when in reality there's
|
|
|
815 |
* hundreds of blank columns beyond. Thus if you pass <code>true</code> to
|
|
|
816 |
* <code>keepTableWidth</code> you'll end up with huge widths.
|
|
|
817 |
*
|
|
|
818 |
* @param firstIndex the first column to remove.
|
|
|
819 |
* @param lastIndex the last column to remove, exclusive.
|
|
|
820 |
* @param keepTableWidth <code>true</code> if the table should be same width after the column
|
|
|
821 |
* change.
|
|
|
822 |
*/
|
|
|
823 |
public final void removeColumn(int firstIndex, int lastIndex, final boolean keepTableWidth) {
|
|
|
824 |
// first check that removeCells() will succeed, so that we avoid an incoherent XML state
|
25 |
ilm |
825 |
final int rowCount = this.getRowCount();
|
|
|
826 |
for (int i = 0; i < rowCount;) {
|
|
|
827 |
final Row<D> r = this.getRow(i);
|
17 |
ilm |
828 |
r.checkRemove(firstIndex, lastIndex);
|
25 |
ilm |
829 |
i += r.getRepeated();
|
17 |
ilm |
830 |
}
|
|
|
831 |
// rm column element
|
25 |
ilm |
832 |
remove(Axis.COLUMN, firstIndex, lastIndex - 1);
|
17 |
ilm |
833 |
// update widths
|
|
|
834 |
updateWidth(keepTableWidth);
|
|
|
835 |
// rm cells
|
25 |
ilm |
836 |
for (int i = 0; i < rowCount;) {
|
|
|
837 |
final Row<D> r = this.getRow(i);
|
17 |
ilm |
838 |
r.removeCells(firstIndex, lastIndex);
|
25 |
ilm |
839 |
i += r.getRepeated();
|
17 |
ilm |
840 |
}
|
|
|
841 |
}
|
|
|
842 |
|
80 |
ilm |
843 |
void updateWidth(final boolean keepTableWidth) {
|
|
|
844 |
// tables widths must have a lower precision than columns since they can't be the exact sum
|
|
|
845 |
// of them.
|
|
|
846 |
final Length currentWidth = getWidth().roundDecimalAmount();
|
|
|
847 |
Length newWidth = Length.ZERO;
|
17 |
ilm |
848 |
Column<?> nullWidthCol = null;
|
|
|
849 |
// columns are flattened in ctor: no repeated
|
|
|
850 |
for (final Column<?> col : this.cols) {
|
80 |
ilm |
851 |
final Length colWidth = col.getWidth();
|
|
|
852 |
if (colWidth.isDefined()) {
|
|
|
853 |
assert colWidth.signum() >= 0;
|
|
|
854 |
newWidth = newWidth.add(colWidth);
|
17 |
ilm |
855 |
} else {
|
|
|
856 |
// we cannot compute the newWidth
|
80 |
ilm |
857 |
newWidth = Length.getNone();
|
17 |
ilm |
858 |
nullWidthCol = col;
|
|
|
859 |
break;
|
|
|
860 |
}
|
|
|
861 |
}
|
80 |
ilm |
862 |
// tables widths must have a lower precision than columns since they can't be the exact sum
|
|
|
863 |
// of them.
|
|
|
864 |
newWidth = newWidth.roundDecimalAmount();
|
17 |
ilm |
865 |
// remove all rel-column-width, simpler and Spreadsheet doesn't use them
|
|
|
866 |
// SpreadSheets have no table width
|
80 |
ilm |
867 |
if (keepTableWidth && currentWidth.isDefined()) {
|
17 |
ilm |
868 |
if (nullWidthCol != null)
|
|
|
869 |
throw new IllegalStateException("Cannot keep width since a column has no width : " + nullWidthCol);
|
|
|
870 |
// compute column-width from table width
|
80 |
ilm |
871 |
final Number ratio = currentWidth.divide(newWidth);
|
17 |
ilm |
872 |
// once per style not once per col, otherwise if multiple columns with same styles they
|
|
|
873 |
// all will be affected multiple times
|
|
|
874 |
final Set<ColumnStyle> colStyles = new HashSet<ColumnStyle>();
|
|
|
875 |
for (final Column<?> col : this.cols) {
|
|
|
876 |
colStyles.add(col.getStyle());
|
|
|
877 |
}
|
|
|
878 |
for (final ColumnStyle colStyle : colStyles) {
|
80 |
ilm |
879 |
colStyle.setWidth(colStyle.getWidth().multiply(ratio));
|
17 |
ilm |
880 |
}
|
|
|
881 |
} else {
|
|
|
882 |
// compute table width from column-width
|
|
|
883 |
final TableStyle style = this.getStyle();
|
|
|
884 |
if (style != null) {
|
73 |
ilm |
885 |
if (nullWidthCol == null)
|
|
|
886 |
style.setWidth(newWidth);
|
80 |
ilm |
887 |
else if (currentWidth.isDefined())
|
17 |
ilm |
888 |
throw new IllegalStateException("Cannot update table width since a column has no width : " + nullWidthCol);
|
80 |
ilm |
889 |
// else currentWidth undefined, no inconsistency, nothing to update
|
17 |
ilm |
890 |
}
|
73 |
ilm |
891 |
// either there's no table width or it's positive and equal to the sum of its columns
|
80 |
ilm |
892 |
assert style == null || style.getWidth().isNone() || (!newWidth.isNone() && newWidth.compareTo(style.getWidth()) == 0);
|
17 |
ilm |
893 |
for (final Column<?> col : this.cols) {
|
|
|
894 |
final ColumnStyle colStyle = col.getStyle();
|
|
|
895 |
// if no style, nothing to remove
|
|
|
896 |
if (colStyle != null)
|
|
|
897 |
colStyle.rmRelWidth();
|
|
|
898 |
}
|
|
|
899 |
}
|
|
|
900 |
}
|
|
|
901 |
|
|
|
902 |
/**
|
|
|
903 |
* Table width.
|
|
|
904 |
*
|
|
|
905 |
* @return the table width, can be <code>null</code> (table has no style or style has no width,
|
|
|
906 |
* eg in SpreadSheet).
|
|
|
907 |
*/
|
80 |
ilm |
908 |
public final Length getWidth() {
|
|
|
909 |
return this.getStyle().getWidth();
|
17 |
ilm |
910 |
}
|
|
|
911 |
|
73 |
ilm |
912 |
/**
|
|
|
913 |
* Create and add an automatic style.
|
|
|
914 |
*
|
|
|
915 |
* @param amount the column width.
|
|
|
916 |
* @param unit the unit of <code>amount</code>.
|
|
|
917 |
* @return the newly added style.
|
|
|
918 |
*/
|
|
|
919 |
public final ColumnStyle createColumnStyle(final Number amount, final LengthUnit unit) {
|
80 |
ilm |
920 |
return createColumnStyle(amount == null ? Length.getNone() : Length.create(amount, unit));
|
|
|
921 |
}
|
|
|
922 |
|
|
|
923 |
public final ColumnStyle createColumnStyle(final Length l) {
|
73 |
ilm |
924 |
final ColumnStyle colStyle = this.getStyleDesc(ColumnStyle.class).createAutoStyle(this.getODDocument().getPackage());
|
80 |
ilm |
925 |
colStyle.setWidth(l);
|
17 |
ilm |
926 |
return colStyle;
|
|
|
927 |
}
|
|
|
928 |
|
25 |
ilm |
929 |
private final void setCount(final Axis col, final int newSize) {
|
17 |
ilm |
930 |
this.remove(col, newSize, -1);
|
|
|
931 |
}
|
|
|
932 |
|
73 |
ilm |
933 |
private final void remove(final Axis col, final int fromIndex, final int toIndexIncl) {
|
|
|
934 |
this.remove(col, fromIndex, toIndexIncl, true);
|
|
|
935 |
}
|
|
|
936 |
|
17 |
ilm |
937 |
// both inclusive
|
73 |
ilm |
938 |
private final void remove(final Axis col, final int fromIndex, final int toIndexIncl, final boolean updateList) {
|
|
|
939 |
assert col == Axis.COLUMN || !updateList || toIndexIncl < 0 : "Row index will be wrong";
|
25 |
ilm |
940 |
final List<? extends TableCalcNode<?, ?>> l = col == Axis.COLUMN ? this.cols : this.rows;
|
17 |
ilm |
941 |
final int toIndexValid = CollectionUtils.getValidIndex(l, toIndexIncl);
|
25 |
ilm |
942 |
final int toRemoveCount = toIndexValid - fromIndex + 1;
|
|
|
943 |
int removedCount = 0;
|
|
|
944 |
while (removedCount < toRemoveCount) {
|
|
|
945 |
// works backwards to keep y OK
|
|
|
946 |
final int i = toIndexValid - removedCount;
|
|
|
947 |
final TableCalcNode<?, ?> removed = l.get(i);
|
|
|
948 |
if (removed instanceof Row) {
|
|
|
949 |
final Row<?> r = (Row<?>) removed;
|
|
|
950 |
final int removeFromRepeated = i - Math.max(fromIndex, r.getY()) + 1;
|
|
|
951 |
// removedCount grows each iteration
|
|
|
952 |
assert removeFromRepeated > 0;
|
|
|
953 |
final int newRepeated = r.getRepeated() - removeFromRepeated;
|
|
|
954 |
if (newRepeated == 0)
|
|
|
955 |
removed.getElement().detach();
|
|
|
956 |
else
|
|
|
957 |
r.setRepeated(newRepeated);
|
|
|
958 |
removedCount += removeFromRepeated;
|
|
|
959 |
} else {
|
|
|
960 |
// Columns are always flattened
|
|
|
961 |
removed.getElement().detach();
|
|
|
962 |
removedCount++;
|
|
|
963 |
}
|
17 |
ilm |
964 |
}
|
25 |
ilm |
965 |
// one remove to be efficient
|
73 |
ilm |
966 |
if (updateList)
|
|
|
967 |
l.subList(fromIndex, toIndexValid + 1).clear();
|
17 |
ilm |
968 |
}
|
|
|
969 |
|
|
|
970 |
public final void ensureRowCount(int newSize) {
|
|
|
971 |
if (newSize > this.getRowCount())
|
|
|
972 |
this.setRowCount(newSize);
|
|
|
973 |
}
|
|
|
974 |
|
|
|
975 |
public final void setRowCount(int newSize) {
|
|
|
976 |
this.setRowCount(newSize, -1);
|
|
|
977 |
}
|
|
|
978 |
|
|
|
979 |
/**
|
|
|
980 |
* Changes the row count. If <code>newSize</code> is less than {@link #getRowCount()} extra rows
|
|
|
981 |
* will be chopped off. Otherwise empty cells will be created.
|
|
|
982 |
*
|
|
|
983 |
* @param newSize the new row count.
|
|
|
984 |
* @param rowIndex the index of the row to be copied, -1 for empty row (i.e. default style).
|
|
|
985 |
*/
|
|
|
986 |
public final void setRowCount(int newSize, int rowIndex) {
|
|
|
987 |
final int toGrow = newSize - this.getRowCount();
|
|
|
988 |
if (toGrow < 0) {
|
25 |
ilm |
989 |
setCount(Axis.ROW, newSize);
|
|
|
990 |
} else if (toGrow > 0) {
|
|
|
991 |
final Element elemToClone;
|
|
|
992 |
if (rowIndex < 0) {
|
|
|
993 |
elemToClone = Row.createEmpty(this.getODDocument().getVersion());
|
|
|
994 |
// each row MUST have the same number of columns
|
|
|
995 |
elemToClone.addContent(Cell.createEmpty(this.getODDocument().getVersion(), this.getColumnCount()));
|
|
|
996 |
} else {
|
|
|
997 |
elemToClone = (Element) getRow(rowIndex).getElement().clone();
|
17 |
ilm |
998 |
}
|
25 |
ilm |
999 |
Axis.ROW.setRepeated(elemToClone, toGrow);
|
|
|
1000 |
// as per section 8.1.1 rows are the last elements inside a table
|
|
|
1001 |
this.getElement().addContent(elemToClone);
|
|
|
1002 |
addRow(elemToClone, getRowStyleDesc(), getCellStyleDesc());
|
17 |
ilm |
1003 |
}
|
|
|
1004 |
}
|
|
|
1005 |
|
73 |
ilm |
1006 |
public final void removeRow(int index) {
|
|
|
1007 |
this.removeRows(index, index + 1);
|
|
|
1008 |
}
|
|
|
1009 |
|
|
|
1010 |
/**
|
|
|
1011 |
* Remove rows between the two passed indexes. If the origin of a merged cell is contained in
|
|
|
1012 |
* the rows to remove, then it will be unmerged, otherwise (the origin is before the rows) the
|
|
|
1013 |
* merged cell will just be shrunk.
|
|
|
1014 |
*
|
|
|
1015 |
* @param firstIndex the start index, inclusive.
|
|
|
1016 |
* @param lastIndex the stop index, exclusive.
|
|
|
1017 |
*/
|
|
|
1018 |
public final void removeRows(int firstIndex, int lastIndex) {
|
|
|
1019 |
if (firstIndex < 0)
|
|
|
1020 |
throw new IllegalArgumentException("Negative index " + firstIndex);
|
|
|
1021 |
if (firstIndex == lastIndex)
|
|
|
1022 |
return;
|
|
|
1023 |
if (firstIndex > lastIndex)
|
|
|
1024 |
throw new IllegalArgumentException("First index after last index " + firstIndex + " > " + lastIndex);
|
|
|
1025 |
|
|
|
1026 |
// remove row elements, minding merged cells
|
|
|
1027 |
final int columnCount = getColumnCount();
|
|
|
1028 |
final Set<Point> coverOrigins = new HashSet<Point>();
|
|
|
1029 |
for (int y = firstIndex; y < lastIndex; y++) {
|
|
|
1030 |
for (int x = 0; x < columnCount; x++) {
|
|
|
1031 |
final Cell<D> cell = this.getImmutableCellAt(x, y);
|
|
|
1032 |
if (cell.isCovered()) {
|
|
|
1033 |
coverOrigins.add(this.getCoverOrigin(x, y));
|
|
|
1034 |
// cells spanning inside the removed rows don't need to be unmerged
|
|
|
1035 |
} else if (cell.coversOtherCells() && y + cell.getRowsSpanned() > lastIndex) {
|
|
|
1036 |
this.getCellAt(x, y).unmerge();
|
|
|
1037 |
}
|
|
|
1038 |
}
|
|
|
1039 |
}
|
|
|
1040 |
|
|
|
1041 |
// don't update this.rows as we need them below and indexes would be wrong anyway
|
|
|
1042 |
this.remove(Axis.ROW, firstIndex, lastIndex - 1, false);
|
|
|
1043 |
|
|
|
1044 |
// adjust rows spanned
|
|
|
1045 |
// we haven't yet reset this.rows, so we can use getImmutableCellAt()
|
|
|
1046 |
for (final Point coverOrigin : coverOrigins) {
|
|
|
1047 |
// if the origin is inside the removed rows, then the whole merged cell was removed
|
|
|
1048 |
// (if the merged cell extended past lastIndex, it got unmerged above and wasn't added
|
|
|
1049 |
// to the set)
|
|
|
1050 |
if (coverOrigin.y >= firstIndex) {
|
|
|
1051 |
assert this.getImmutableCellAt(coverOrigin.x, coverOrigin.y).getElement().getDocument() == null;
|
|
|
1052 |
continue;
|
|
|
1053 |
}
|
|
|
1054 |
final Cell<D> coverOriginCell = this.getImmutableCellAt(coverOrigin.x, coverOrigin.y);
|
|
|
1055 |
final int rowsSpanned = coverOriginCell.getRowsSpanned();
|
|
|
1056 |
// otherwise the cover origin is between firstIndex and lastIndex and should have been
|
|
|
1057 |
// unmerged
|
|
|
1058 |
assert rowsSpanned > 1;
|
|
|
1059 |
final int stopY = coverOrigin.y + rowsSpanned;
|
|
|
1060 |
final int toRemove = Math.min(lastIndex, stopY) - firstIndex;
|
|
|
1061 |
assert toRemove > 0 && toRemove < rowsSpanned;
|
|
|
1062 |
coverOriginCell.getElement().setAttribute("number-rows-spanned", (rowsSpanned - toRemove) + "", getTABLE());
|
|
|
1063 |
}
|
|
|
1064 |
|
|
|
1065 |
// remove empty table-row-group
|
|
|
1066 |
// since indexes are stored in TableGroup, we can do it after having removed rows
|
|
|
1067 |
final Set<TableGroup> groups = new HashSet<TableGroup>();
|
|
|
1068 |
for (int y = firstIndex; y < lastIndex; y++) {
|
|
|
1069 |
groups.add(this.getRowGroupAt(y));
|
|
|
1070 |
}
|
|
|
1071 |
for (final TableGroup group : groups) {
|
|
|
1072 |
TableGroup g = group;
|
|
|
1073 |
Element elem = g.getElement();
|
|
|
1074 |
while (g != null && elem.getParent() != null && elem.getChildren().size() == 0) {
|
|
|
1075 |
elem.detach();
|
|
|
1076 |
g = g.getParent();
|
|
|
1077 |
elem = g == null ? null : g.getElement();
|
|
|
1078 |
}
|
|
|
1079 |
}
|
|
|
1080 |
|
|
|
1081 |
// recreate list from XML
|
|
|
1082 |
this.readRows();
|
|
|
1083 |
}
|
|
|
1084 |
|
17 |
ilm |
1085 |
// *** table models
|
|
|
1086 |
|
|
|
1087 |
public final SheetTableModel<D> getTableModel(final int column, final int row) {
|
|
|
1088 |
return new SheetTableModel<D>(this, row, column);
|
|
|
1089 |
}
|
|
|
1090 |
|
|
|
1091 |
public final SheetTableModel<D> getTableModel(final int column, final int row, final int lastCol, final int lastRow) {
|
|
|
1092 |
return new SheetTableModel<D>(this, row, column, lastRow, lastCol);
|
|
|
1093 |
}
|
|
|
1094 |
|
|
|
1095 |
public final MutableTableModel<D> getMutableTableModel(final int column, final int row) {
|
|
|
1096 |
return new MutableTableModel<D>(this, row, column);
|
|
|
1097 |
}
|
|
|
1098 |
|
|
|
1099 |
/**
|
|
|
1100 |
* Return the table from <code>start</code> to <code>end</code> inclusive.
|
|
|
1101 |
*
|
|
|
1102 |
* @param start the first cell of the result.
|
|
|
1103 |
* @param end the last cell of the result.
|
|
|
1104 |
* @return the table.
|
|
|
1105 |
*/
|
|
|
1106 |
public final MutableTableModel<D> getMutableTableModel(final Point start, final Point end) {
|
|
|
1107 |
// +1 since exclusive
|
|
|
1108 |
return new MutableTableModel<D>(this, start.y, start.x, end.y + 1, end.x + 1);
|
|
|
1109 |
}
|
|
|
1110 |
|
|
|
1111 |
public final void merge(TableModel t, final int column, final int row) {
|
|
|
1112 |
this.merge(t, column, row, false);
|
|
|
1113 |
}
|
|
|
1114 |
|
|
|
1115 |
/**
|
|
|
1116 |
* Merges t into this sheet at the specified point.
|
|
|
1117 |
*
|
|
|
1118 |
* @param t the data to be merged.
|
|
|
1119 |
* @param column the column t will be merged at.
|
|
|
1120 |
* @param row the row t will be merged at.
|
|
|
1121 |
* @param includeColNames if <code>true</code> the column names of t will also be merged.
|
|
|
1122 |
*/
|
|
|
1123 |
public final void merge(TableModel t, final int column, final int row, final boolean includeColNames) {
|
|
|
1124 |
final int offset = (includeColNames ? 1 : 0);
|
|
|
1125 |
// the columns must be first, see section 8.1.1 of v1.1
|
|
|
1126 |
this.ensureColumnCount(column + t.getColumnCount());
|
|
|
1127 |
this.ensureRowCount(row + t.getRowCount() + offset);
|
|
|
1128 |
final TableModel thisModel = this.getMutableTableModel(column, row);
|
|
|
1129 |
if (includeColNames) {
|
|
|
1130 |
for (int x = 0; x < t.getColumnCount(); x++) {
|
|
|
1131 |
thisModel.setValueAt(t.getColumnName(x), 0, x);
|
|
|
1132 |
}
|
|
|
1133 |
}
|
|
|
1134 |
for (int y = 0; y < t.getRowCount(); y++) {
|
|
|
1135 |
for (int x = 0; x < t.getColumnCount(); x++) {
|
|
|
1136 |
final Object value = t.getValueAt(y, x);
|
|
|
1137 |
thisModel.setValueAt(value, y + offset, x);
|
|
|
1138 |
}
|
|
|
1139 |
}
|
|
|
1140 |
}
|
|
|
1141 |
|
73 |
ilm |
1142 |
/**
|
|
|
1143 |
* All ranges defined in this table.
|
|
|
1144 |
*
|
|
|
1145 |
* @return the names.
|
|
|
1146 |
* @see SpreadSheet#getRangesNames()
|
|
|
1147 |
*/
|
|
|
1148 |
public final Set<String> getRangesNames() {
|
|
|
1149 |
return SpreadSheet.getRangesNames(getElement(), getTABLE());
|
|
|
1150 |
}
|
|
|
1151 |
|
|
|
1152 |
/**
|
|
|
1153 |
* Get a named range in this table.
|
|
|
1154 |
*
|
|
|
1155 |
* @param name the name of the range.
|
|
|
1156 |
* @return a named range, or <code>null</code> if the passed name doesn't exist.
|
|
|
1157 |
* @see #getRangesNames()
|
|
|
1158 |
* @see SpreadSheet#getRange(String)
|
|
|
1159 |
*/
|
|
|
1160 |
public final Range getRange(String name) {
|
|
|
1161 |
return SpreadSheet.getRange(getElement(), getTABLE(), name);
|
|
|
1162 |
}
|
|
|
1163 |
|
|
|
1164 |
public final MutableTableModel<D> getTableModel(String name) {
|
|
|
1165 |
final Range points = getRange(name);
|
|
|
1166 |
if (points == null)
|
|
|
1167 |
return null;
|
|
|
1168 |
if (points.spanSheets())
|
|
|
1169 |
throw new IllegalStateException("different sheet names: " + points.getStartSheet() + " != " + points.getEndSheet());
|
|
|
1170 |
return this.getMutableTableModel(points.getStartPoint(), points.getEndPoint());
|
|
|
1171 |
}
|
|
|
1172 |
|
17 |
ilm |
1173 |
// * UsedRange & CurrentRegion
|
|
|
1174 |
|
|
|
1175 |
/**
|
|
|
1176 |
* The range that covers all used cells.
|
|
|
1177 |
*
|
|
|
1178 |
* @return the range that covers all used cells, <code>null</code> if the table is completely
|
|
|
1179 |
* empty.
|
|
|
1180 |
*/
|
|
|
1181 |
public final Range getUsedRange() {
|
|
|
1182 |
return this.getUsedRange(false);
|
|
|
1183 |
}
|
|
|
1184 |
|
|
|
1185 |
/**
|
|
|
1186 |
* The range that covers all used cells.
|
|
|
1187 |
*
|
|
|
1188 |
* @param checkStyle <code>true</code> to check the background and borders in addition to the
|
|
|
1189 |
* content.
|
|
|
1190 |
* @return the range that covers all used cells, <code>null</code> if the table is completely
|
|
|
1191 |
* blank.
|
|
|
1192 |
*/
|
|
|
1193 |
public final Range getUsedRange(boolean checkStyle) {
|
|
|
1194 |
int minX = -1, minY = -1, maxX = -1, maxY = -1;
|
|
|
1195 |
final int colCount = this.getColumnCount();
|
|
|
1196 |
final int rowCount = this.getRowCount();
|
|
|
1197 |
for (int x = 0; x < colCount; x++) {
|
|
|
1198 |
for (int y = 0; y < rowCount; y++) {
|
|
|
1199 |
if (!this.isCellBlank(x, y, checkStyle)) {
|
|
|
1200 |
if (minX < 0 || x < minX)
|
|
|
1201 |
minX = x;
|
|
|
1202 |
if (minY < 0 || y < minY)
|
|
|
1203 |
minY = y;
|
|
|
1204 |
|
|
|
1205 |
if (maxX < 0 || x > maxX)
|
|
|
1206 |
maxX = x;
|
|
|
1207 |
if (maxY < 0 || y > maxY)
|
|
|
1208 |
maxY = y;
|
|
|
1209 |
}
|
|
|
1210 |
}
|
|
|
1211 |
}
|
|
|
1212 |
return minX < 0 ? null : new Range(getName(), new Point(minX, minY), new Point(maxX, maxY));
|
|
|
1213 |
}
|
|
|
1214 |
|
|
|
1215 |
protected final boolean isCellBlank(final int x, int y, boolean checkStyle) {
|
|
|
1216 |
if (!getImmutableCellAt(x, y).isEmpty())
|
|
|
1217 |
return false;
|
|
|
1218 |
|
|
|
1219 |
if (checkStyle) {
|
73 |
ilm |
1220 |
final StyleTableCellProperties tableCellProperties = this.getTableCellPropertiesAt(x, y);
|
|
|
1221 |
return tableCellProperties == null || (tableCellProperties.getBackgroundColor() == null && tableCellProperties.getBorders().isEmpty());
|
17 |
ilm |
1222 |
} else {
|
|
|
1223 |
return true;
|
|
|
1224 |
}
|
|
|
1225 |
}
|
|
|
1226 |
|
|
|
1227 |
private class RegionExplorer {
|
|
|
1228 |
|
|
|
1229 |
private final boolean checkStyle;
|
|
|
1230 |
private final int rowCount, colCount;
|
|
|
1231 |
protected int minX, minY, maxX, maxY;
|
|
|
1232 |
|
|
|
1233 |
public RegionExplorer(final int startX, final int startY, final boolean checkStyle) {
|
|
|
1234 |
this.rowCount = getRowCount();
|
|
|
1235 |
this.colCount = getColumnCount();
|
|
|
1236 |
this.minX = this.maxX = startX;
|
|
|
1237 |
this.minY = this.maxY = startY;
|
|
|
1238 |
this.checkStyle = checkStyle;
|
|
|
1239 |
}
|
|
|
1240 |
|
|
|
1241 |
public boolean canXDecrement() {
|
|
|
1242 |
return this.minX > 0;
|
|
|
1243 |
}
|
|
|
1244 |
|
|
|
1245 |
public boolean canYDecrement() {
|
|
|
1246 |
return this.minY > 0;
|
|
|
1247 |
}
|
|
|
1248 |
|
|
|
1249 |
public boolean canXIncrement() {
|
|
|
1250 |
return this.maxX < this.colCount - 1;
|
|
|
1251 |
}
|
|
|
1252 |
|
|
|
1253 |
public boolean canYIncrement() {
|
|
|
1254 |
return this.maxY < this.rowCount - 1;
|
|
|
1255 |
}
|
|
|
1256 |
|
|
|
1257 |
private boolean checkRow(final boolean upper) {
|
|
|
1258 |
if (upper && this.canYDecrement() || !upper && this.canYIncrement()) {
|
|
|
1259 |
final int y = upper ? this.minY - 1 : this.maxY + 1;
|
|
|
1260 |
final int start = this.canXDecrement() ? this.minX - 1 : this.minX;
|
|
|
1261 |
final int stop = this.canXIncrement() ? this.maxX + 1 : this.maxX;
|
|
|
1262 |
for (int x = start; x <= stop; x++) {
|
|
|
1263 |
if (!isCellBlank(x, y, this.checkStyle)) {
|
|
|
1264 |
if (upper)
|
|
|
1265 |
this.minY = y;
|
|
|
1266 |
else
|
|
|
1267 |
this.maxY = y;
|
|
|
1268 |
if (x < this.minX)
|
|
|
1269 |
this.minX = x;
|
|
|
1270 |
if (x > this.maxX)
|
|
|
1271 |
this.maxX = x;
|
|
|
1272 |
return true;
|
|
|
1273 |
}
|
|
|
1274 |
}
|
|
|
1275 |
}
|
|
|
1276 |
return false;
|
|
|
1277 |
}
|
|
|
1278 |
|
|
|
1279 |
// sans corners (checked by checkRow())
|
|
|
1280 |
private boolean checkCol(final boolean left) {
|
|
|
1281 |
if (left && this.canXDecrement() || !left && this.canXIncrement()) {
|
|
|
1282 |
final int x = left ? this.minX - 1 : this.maxX + 1;
|
|
|
1283 |
for (int y = this.minY; y <= this.maxY; y++) {
|
|
|
1284 |
if (!isCellBlank(x, y, this.checkStyle)) {
|
|
|
1285 |
if (left)
|
|
|
1286 |
this.minX = x;
|
|
|
1287 |
else
|
|
|
1288 |
this.maxX = x;
|
|
|
1289 |
return true;
|
|
|
1290 |
}
|
|
|
1291 |
}
|
|
|
1292 |
}
|
|
|
1293 |
return false;
|
|
|
1294 |
}
|
|
|
1295 |
|
|
|
1296 |
private final boolean checkFrame() {
|
|
|
1297 |
return this.checkRow(true) || this.checkRow(false) || this.checkCol(true) || this.checkCol(false);
|
|
|
1298 |
}
|
|
|
1299 |
|
|
|
1300 |
public final Range getCurrentRegion() {
|
|
|
1301 |
while (this.checkFrame())
|
|
|
1302 |
;// bounded by table size
|
25 |
ilm |
1303 |
return new Range(getName(), new Point(this.minX, this.minY), new Point(this.maxX, this.maxY));
|
17 |
ilm |
1304 |
}
|
|
|
1305 |
}
|
|
|
1306 |
|
|
|
1307 |
public final Range getCurrentRegion(String ref) {
|
|
|
1308 |
return this.getCurrentRegion(ref, false);
|
|
|
1309 |
}
|
|
|
1310 |
|
|
|
1311 |
public final Range getCurrentRegion(String ref, boolean checkStyle) {
|
|
|
1312 |
final Point p = resolveHint(ref);
|
|
|
1313 |
return this.getCurrentRegion(p.x, p.y, checkStyle);
|
|
|
1314 |
}
|
|
|
1315 |
|
|
|
1316 |
/**
|
|
|
1317 |
* The smallest range containing the passed cell completely surrounded by empty rows and
|
|
|
1318 |
* columns.
|
|
|
1319 |
*
|
|
|
1320 |
* @param startX x coordinate.
|
|
|
1321 |
* @param startY y coordinate.
|
|
|
1322 |
* @return the smallest range containing the passed cell.
|
25 |
ilm |
1323 |
* @see <a href="http://msdn.microsoft.com/library/aa214248(v=office.11).aspx">CurrentRegion
|
|
|
1324 |
* Property</a>
|
17 |
ilm |
1325 |
*/
|
|
|
1326 |
public final Range getCurrentRegion(final int startX, final int startY) {
|
|
|
1327 |
return this.getCurrentRegion(startX, startY, false);
|
|
|
1328 |
}
|
|
|
1329 |
|
|
|
1330 |
public final Range getCurrentRegion(final int startX, final int startY, final boolean checkStyle) {
|
|
|
1331 |
return new RegionExplorer(startX, startY, checkStyle).getCurrentRegion();
|
|
|
1332 |
}
|
|
|
1333 |
|
|
|
1334 |
// *** static
|
|
|
1335 |
|
|
|
1336 |
/**
|
|
|
1337 |
* Convert string coordinates into numeric ones.
|
|
|
1338 |
*
|
|
|
1339 |
* @param ref the string address, eg "$AA$34" or "AA34".
|
|
|
1340 |
* @return the numeric coordinates or <code>null</code> if <code>ref</code> is not valid, eg
|
|
|
1341 |
* {26, 33}.
|
|
|
1342 |
*/
|
|
|
1343 |
static final Point resolve(String ref) {
|
|
|
1344 |
final Matcher matcher = SpreadSheet.minCellPattern.matcher(ref);
|
|
|
1345 |
if (!matcher.matches())
|
|
|
1346 |
return null;
|
|
|
1347 |
return resolve(matcher.group(1), matcher.group(2));
|
|
|
1348 |
}
|
|
|
1349 |
|
|
|
1350 |
/**
|
|
|
1351 |
* Convert string coordinates into numeric ones. ATTN this method does no checks.
|
|
|
1352 |
*
|
|
|
1353 |
* @param letters the column, eg "AA".
|
|
|
1354 |
* @param digits the row, eg "34".
|
|
|
1355 |
* @return the numeric coordinates, eg {26, 33}.
|
|
|
1356 |
*/
|
|
|
1357 |
static final Point resolve(final String letters, final String digits) {
|
|
|
1358 |
return new Point(toInt(letters), Integer.parseInt(digits) - 1);
|
|
|
1359 |
}
|
|
|
1360 |
|
|
|
1361 |
// "AA" => 26
|
|
|
1362 |
static final int toInt(String col) {
|
|
|
1363 |
if (col.length() < 1)
|
|
|
1364 |
throw new IllegalArgumentException("x cannot be empty");
|
|
|
1365 |
col = col.toUpperCase();
|
|
|
1366 |
|
|
|
1367 |
int x = 0;
|
|
|
1368 |
for (int i = 0; i < col.length(); i++) {
|
|
|
1369 |
x = x * 26 + (col.charAt(i) - 'A' + 1);
|
|
|
1370 |
}
|
|
|
1371 |
|
|
|
1372 |
// zero based
|
|
|
1373 |
return x - 1;
|
|
|
1374 |
}
|
|
|
1375 |
|
41 |
ilm |
1376 |
public static final String toStr(int col) {
|
17 |
ilm |
1377 |
if (col < 0)
|
|
|
1378 |
throw new IllegalArgumentException("negative column : " + col);
|
|
|
1379 |
// one based (i.e. 0 is A)
|
|
|
1380 |
col++;
|
|
|
1381 |
|
|
|
1382 |
final int radix = 26;
|
|
|
1383 |
final StringBuilder chars = new StringBuilder(4);
|
|
|
1384 |
while (col > 0) {
|
|
|
1385 |
chars.append((char) ('A' + ((col - 1) % radix)));
|
|
|
1386 |
col = (col - 1) / radix;
|
|
|
1387 |
}
|
|
|
1388 |
|
|
|
1389 |
return chars.reverse().toString();
|
|
|
1390 |
}
|
|
|
1391 |
|
|
|
1392 |
/**
|
|
|
1393 |
* Convert numeric coordinates into string ones.
|
|
|
1394 |
*
|
|
|
1395 |
* @param p the numeric coordinates, e.g. {26, 33}.
|
|
|
1396 |
* @return the string address, e.g. "AA34".
|
|
|
1397 |
*/
|
|
|
1398 |
static final String getAddress(Point p) {
|
156 |
ilm |
1399 |
return getAddress(p.x, p.y);
|
17 |
ilm |
1400 |
}
|
156 |
ilm |
1401 |
|
|
|
1402 |
static final String getAddress(final int x, final int y) {
|
|
|
1403 |
if (x < 0 || y < 0)
|
|
|
1404 |
throw new IllegalArgumentException("negative coordinates : " + x + ":" + y);
|
|
|
1405 |
return toStr(x) + (y + 1);
|
|
|
1406 |
}
|
17 |
ilm |
1407 |
}
|