/* * Licensed to the Apache Software Foundation (ASF) Under one or more * contributor license agreements. See the NOTICE file distributed with * this work for Additional information regarding copyright ownership. * The ASF licenses this file to You Under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed Under the License is distributed on an "AS Is" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations Under the License. */ namespace HH.WMS.Utils.NPOI.HSSF.Record.Aggregates { using System; using System.Text; using System.Collections; using HH.WMS.Utils.NPOI.Util; using HH.WMS.Utils.NPOI.HSSF.Record; using HH.WMS.Utils.NPOI.HSSF.Model; using System.Globalization; /// /// @author Glen Stampoultzis /// public class ColumnInfoRecordsAggregate : RecordAggregate { private class CIRComparator : IComparer { public static IComparer instance = new CIRComparator(); private CIRComparator() { // enforce singleton } public int Compare(Object a, Object b) { return CompareColInfos((ColumnInfoRecord)a, (ColumnInfoRecord)b); } public static int CompareColInfos(ColumnInfoRecord a, ColumnInfoRecord b) { return a.FirstColumn - b.FirstColumn; } } // int size = 0; ArrayList records = null; /// /// Initializes a new instance of the class. /// public ColumnInfoRecordsAggregate() { records = new ArrayList(); } /// /// Initializes a new instance of the class. /// /// The rs. public ColumnInfoRecordsAggregate(RecordStream rs): this() { bool isInOrder = true; ColumnInfoRecord cirPrev = null; while (rs.PeekNextClass() == typeof(ColumnInfoRecord)) { ColumnInfoRecord cir = (ColumnInfoRecord)rs.GetNext(); records.Add(cir); if (cirPrev != null && CIRComparator.CompareColInfos(cirPrev, cir) > 0) { isInOrder = false; } cirPrev = cir; } if (records.Count < 1) { throw new InvalidOperationException("No column info records found"); } if (!isInOrder) { records.Sort(CIRComparator.instance); } } /** It's an aggregate... just made something up */ public override short Sid { get { return -1012; } } /// /// Gets the num columns. /// /// The num columns. public int NumColumns { get { return records.Count; } } /// /// Gets the size of the record. /// /// The size of the record. public override int RecordSize { get { int size = 0; for (IEnumerator iterator = records.GetEnumerator(); iterator.MoveNext(); ) size += ((ColumnInfoRecord)iterator.Current).RecordSize; return size; } } public IEnumerator GetEnumerator() { return records.GetEnumerator(); } /** * Performs a deep Clone of the record */ public Object Clone() { ColumnInfoRecordsAggregate rec = new ColumnInfoRecordsAggregate(); for (int k = 0; k < records.Count; k++) { ColumnInfoRecord ci = (ColumnInfoRecord)records[k]; ci = (ColumnInfoRecord)ci.Clone(); rec.records.Add(ci); } return rec; } /// /// Inserts a column into the aggregate (at the end of the list). /// /// The column. public void InsertColumn(ColumnInfoRecord col) { records.Add(col); records.Sort(CIRComparator.instance); } /// /// Inserts a column into the aggregate (at the position specified /// by index /// /// The index. /// The columninfo. public void InsertColumn(int idx, ColumnInfoRecord col) { records.Insert(idx, col); } /// /// called by the class that is responsible for writing this sucker. /// Subclasses should implement this so that their data is passed back in a /// byte array. /// /// offset to begin writing at /// byte array containing instance data /// number of bytes written public override int Serialize(int offset, byte[] data) { IEnumerator itr = records.GetEnumerator(); int pos = offset; while (itr.MoveNext()) { pos += ((Record)itr.Current).Serialize(pos, data); } return pos - offset; } /// /// Visit each of the atomic BIFF records contained in this {@link RecordAggregate} in the order /// that they should be written to file. Implementors may or may not return the actual /// Records being used to manage POI's internal implementation. Callers should not /// assume either way, and therefore only attempt to modify those Records after cloning /// /// public override void VisitContainedRecords(RecordVisitor rv) { int nItems = records.Count; if (nItems < 1) { return; } ColumnInfoRecord cirPrev = null; for (int i = 0; i < nItems; i++) { ColumnInfoRecord cir = (ColumnInfoRecord)records[i]; rv.VisitRecord(cir); if (cirPrev != null && CIRComparator.CompareColInfos(cirPrev, cir) > 0) { // Excel probably wouldn't mind, but there is much logic in this class // that assumes the column info records are kept in order throw new InvalidOperationException("Column info records are out of order"); } cirPrev = cir; } } /// /// Finds the start of column outline group. /// /// The idx. /// public int FindStartOfColumnOutlineGroup(int idx) { // Find the start of the Group. ColumnInfoRecord columnInfo = (ColumnInfoRecord)records[idx]; int level = columnInfo.OutlineLevel; while (idx != 0) { ColumnInfoRecord prevColumnInfo = (ColumnInfoRecord)records[idx - 1]; if (columnInfo.FirstColumn - 1 == prevColumnInfo.LastColumn) { if (prevColumnInfo.OutlineLevel < level) { break; } idx--; columnInfo = prevColumnInfo; } else { break; } } return idx; } /// /// Finds the end of column outline group. /// /// The idx. /// public int FindEndOfColumnOutlineGroup(int idx) { // Find the end of the Group. ColumnInfoRecord columnInfo = (ColumnInfoRecord)records[idx]; int level = columnInfo.OutlineLevel; while (idx < records.Count - 1) { ColumnInfoRecord nextColumnInfo = (ColumnInfoRecord)records[idx + 1]; if (columnInfo.LastColumn + 1 == nextColumnInfo.FirstColumn) { if (nextColumnInfo.OutlineLevel < level) { break; } idx++; columnInfo = nextColumnInfo; } else { break; } } return idx; } /// /// Gets the col info. /// /// The idx. /// public ColumnInfoRecord GetColInfo(int idx) { return (ColumnInfoRecord)records[idx]; } /// /// Determines whether [is column group collapsed] [the specified idx]. /// /// The idx. /// /// true if [is column group collapsed] [the specified idx]; otherwise, false. /// public bool IsColumnGroupCollapsed(int idx) { int endOfOutlineGroupIdx = FindEndOfColumnOutlineGroup(idx); int nextColInfoIx = endOfOutlineGroupIdx + 1; if (nextColInfoIx >= records.Count) { return false; } ColumnInfoRecord nextColInfo = GetColInfo(nextColInfoIx); if (!GetColInfo(endOfOutlineGroupIdx).IsAdjacentBefore(nextColInfo)) { return false; } return nextColInfo.IsCollapsed; } /// /// Determines whether [is column group hidden by parent] [the specified idx]. /// /// The idx. /// /// true if [is column group hidden by parent] [the specified idx]; otherwise, false. /// public bool IsColumnGroupHiddenByParent(int idx) { // Look out outline details of end int endLevel = 0; bool endHidden = false; int endOfOutlineGroupIdx = FindEndOfColumnOutlineGroup(idx); if (endOfOutlineGroupIdx < records.Count) { ColumnInfoRecord nextInfo = GetColInfo(endOfOutlineGroupIdx + 1); if (GetColInfo(endOfOutlineGroupIdx).IsAdjacentBefore(nextInfo)) { endLevel = nextInfo.OutlineLevel; endHidden = nextInfo.IsHidden; } } // Look out outline details of start int startLevel = 0; bool startHidden = false; int startOfOutlineGroupIdx = FindStartOfColumnOutlineGroup(idx); if (startOfOutlineGroupIdx > 0) { ColumnInfoRecord prevInfo = GetColInfo(startOfOutlineGroupIdx - 1); if (prevInfo.IsAdjacentBefore(GetColInfo(startOfOutlineGroupIdx))) { startLevel = prevInfo.OutlineLevel; startHidden = prevInfo.IsHidden; } } if (endLevel > startLevel) { return endHidden; } return startHidden; } /// /// Collapses the column. /// /// The column number. public void CollapseColumn(int columnNumber) { int idx = FindColInfoIdx(columnNumber, 0); if (idx == -1) return; // Find the start of the group. int groupStartColInfoIx = FindStartOfColumnOutlineGroup(idx); ColumnInfoRecord columnInfo = GetColInfo(groupStartColInfoIx); // Hide all the columns until the end of the group int lastColIx = SetGroupHidden(groupStartColInfoIx, columnInfo.OutlineLevel, true); // Write collapse field SetColumn(lastColIx + 1, null, null, null, null, true); } /// /// Expands the column. /// /// The column number. public void ExpandColumn(int columnNumber) { int idx = FindColInfoIdx(columnNumber, 0); if (idx == -1) return; // If it is already exapanded do nothing. if (!IsColumnGroupCollapsed(idx)) return; // Find the start of the Group. int startIdx = FindStartOfColumnOutlineGroup(idx); ColumnInfoRecord columnInfo = GetColInfo(startIdx); // Find the end of the Group. int endIdx = FindEndOfColumnOutlineGroup(idx); ColumnInfoRecord endColumnInfo = GetColInfo(endIdx); // expand: // colapsed bit must be UnSet // hidden bit Gets UnSet _if_ surrounding Groups are expanded you can determine // this by looking at the hidden bit of the enclosing Group. You will have // to look at the start and the end of the current Group to determine which // is the enclosing Group // hidden bit only is altered for this outline level. ie. don't Uncollapse contained Groups if (!IsColumnGroupHiddenByParent(idx)) { for (int i = startIdx; i <= endIdx; i++) { if (columnInfo.OutlineLevel == GetColInfo(i).OutlineLevel) GetColInfo(i).IsHidden = false; } } // Write collapse field SetColumn(columnInfo.LastColumn + 1, null, null, null, null, false); } /** * Sets all non null fields into the ci parameter. */ private static void SetColumnInfoFields(ColumnInfoRecord ci, short? xfStyle, int? width, int? level, Boolean? hidden, Boolean? collapsed) { if (xfStyle != null) { ci.XFIndex = Convert.ToInt16(xfStyle, CultureInfo.InvariantCulture); } if (width != null) { ci.ColumnWidth = Convert.ToInt32(width, CultureInfo.InvariantCulture); } if (level != null) { ci.OutlineLevel = (short)level; } if (hidden != null) { ci.IsHidden = Convert.ToBoolean(hidden, CultureInfo.InvariantCulture); } if (collapsed != null) { ci.IsCollapsed = Convert.ToBoolean(collapsed, CultureInfo.InvariantCulture); } } /// /// Attempts to merge the col info record at the specified index /// with either or both of its neighbours /// /// The col info ix. private void AttemptMergeColInfoRecords(int colInfoIx) { int nRecords = records.Count; if (colInfoIx < 0 || colInfoIx >= nRecords) { throw new ArgumentException("colInfoIx " + colInfoIx + " is out of range (0.." + (nRecords - 1) + ")"); } ColumnInfoRecord currentCol = GetColInfo(colInfoIx); int nextIx = colInfoIx + 1; if (nextIx < nRecords) { if (MergeColInfoRecords(currentCol, GetColInfo(nextIx))) { records.RemoveAt(nextIx); } } if (colInfoIx > 0) { if (MergeColInfoRecords(GetColInfo(colInfoIx - 1), currentCol)) { records.RemoveAt(colInfoIx); } } } /** * merges two column info records (if they are adjacent and have the same formatting, etc) * @return false if the two column records could not be merged */ private static bool MergeColInfoRecords(ColumnInfoRecord ciA, ColumnInfoRecord ciB) { if (ciA.IsAdjacentBefore(ciB) && ciA.FormatMatches(ciB)) { ciA.LastColumn = ciB.LastColumn; return true; } return false; } /// /// Sets all adjacent columns of the same outline level to the specified hidden status. /// /// the col info index of the start of the outline group. /// The level. /// The hidden. /// the column index of the last column in the outline group private int SetGroupHidden(int pIdx, int level, bool hidden) { int idx = pIdx; ColumnInfoRecord columnInfo = GetColInfo(idx); while (idx < records.Count) { columnInfo.IsHidden = (hidden); if (idx + 1 < records.Count) { ColumnInfoRecord nextColumnInfo = GetColInfo(idx + 1); if (!columnInfo.IsAdjacentBefore(nextColumnInfo)) { break; } if (nextColumnInfo.OutlineLevel < level) { break; } columnInfo = nextColumnInfo; } idx++; } return columnInfo.LastColumn; } /// /// Sets the column. /// /// The target column ix. /// Index of the xf. /// The width. /// The level. /// The hidden. /// The collapsed. public void SetColumn(int targetColumnIx, short? xfIndex, int? width, int? level, bool? hidden, bool? collapsed) { ColumnInfoRecord ci = null; int k = 0; for (k = 0; k < records.Count; k++) { ColumnInfoRecord tci = (ColumnInfoRecord)records[k]; if (tci.ContainsColumn(targetColumnIx)) { ci = tci; break; } if (tci.FirstColumn > targetColumnIx) { // call targetColumnIx infos after k are for later targetColumnIxs break; // exit now so k will be the correct insert pos } } if (ci == null) { // okay so there IsN'T a targetColumnIx info record that cover's this targetColumnIx so lets Create one! ColumnInfoRecord nci = new ColumnInfoRecord(); nci.FirstColumn = targetColumnIx; nci.LastColumn = targetColumnIx; SetColumnInfoFields(nci, xfIndex, width, level, hidden, collapsed); InsertColumn(k, nci); AttemptMergeColInfoRecords(k); return; } bool styleChanged = ci.XFIndex != xfIndex; bool widthChanged = ci.ColumnWidth != width; bool levelChanged = ci.OutlineLevel != level; bool hiddenChanged = ci.IsHidden != hidden; bool collapsedChanged = ci.IsCollapsed != collapsed; bool targetColumnIxChanged = styleChanged || widthChanged || levelChanged || hiddenChanged || collapsedChanged; if (!targetColumnIxChanged) { // do nothing...nothing Changed. return; } if ((ci.FirstColumn == targetColumnIx) && (ci.LastColumn == targetColumnIx)) { // if its only for this cell then // ColumnInfo ci for a single column, the target column SetColumnInfoFields(ci, xfIndex, width, level, hidden, collapsed); AttemptMergeColInfoRecords(k); return; } if ((ci.FirstColumn == targetColumnIx) || (ci.LastColumn == targetColumnIx)) { // The target column is at either end of the multi-column ColumnInfo ci // we'll just divide the info and create a new one if (ci.FirstColumn == targetColumnIx) { ci.FirstColumn = targetColumnIx + 1; } else { ci.LastColumn = targetColumnIx - 1; k++; // adjust insert pos to insert after } ColumnInfoRecord nci = CopyColInfo(ci); nci.FirstColumn = targetColumnIx; nci.LastColumn = targetColumnIx; SetColumnInfoFields(nci, xfIndex, width, level, hidden, collapsed); InsertColumn(k, nci); AttemptMergeColInfoRecords(k); } else { //split to 3 records ColumnInfoRecord ciStart = ci; ColumnInfoRecord ciMid = CopyColInfo(ci); ColumnInfoRecord ciEnd = CopyColInfo(ci); int lastcolumn = ci.LastColumn; ciStart.LastColumn = (targetColumnIx - 1); ciMid.FirstColumn=(targetColumnIx); ciMid.LastColumn=(targetColumnIx); SetColumnInfoFields(ciMid, xfIndex, width, level, hidden, collapsed); InsertColumn(++k, ciMid); ciEnd.FirstColumn = (targetColumnIx + 1); ciEnd.LastColumn = (lastcolumn); InsertColumn(++k, ciEnd); // no need to attemptMergeColInfoRecords because we // know both on each side are different } } private ColumnInfoRecord CopyColInfo(ColumnInfoRecord ci) { return (ColumnInfoRecord)ci.Clone(); } /** * Sets all non null fields into the ci parameter. */ private void SetColumnInfoFields(ColumnInfoRecord ci, short xfStyle, short width, int level, bool hidden, bool collapsed) { ci.XFIndex = (xfStyle); ci.ColumnWidth = (width); ci.OutlineLevel = (short)level; ci.IsHidden = (hidden); ci.IsCollapsed = (collapsed); } /// /// Collapses the col info records. /// /// The column index. public void CollapseColInfoRecords(int columnIdx) { if (columnIdx == 0) return; ColumnInfoRecord previousCol = (ColumnInfoRecord)records[columnIdx - 1]; ColumnInfoRecord currentCol = (ColumnInfoRecord)records[columnIdx]; bool adjacentColumns = previousCol.LastColumn == currentCol.FirstColumn - 1; if (!adjacentColumns) return; bool columnsMatch = previousCol.XFIndex == currentCol.XFIndex && previousCol.Options == currentCol.Options && previousCol.ColumnWidth == currentCol.ColumnWidth; if (columnsMatch) { previousCol.LastColumn = currentCol.LastColumn; records.Remove(columnIdx); } } /// /// Creates an outline Group for the specified columns. /// /// Group from this column (inclusive) /// Group to this column (inclusive) /// if true the Group will be indented by one level;if false indenting will be Removed by one level. public void GroupColumnRange(int fromColumnIx, int toColumnIx, bool indent) { int colInfoSearchStartIdx = 0; // optimization to speed up the search for col infos for (int i = fromColumnIx; i <= toColumnIx; i++) { int level = 1; int colInfoIdx = FindColInfoIdx(i, colInfoSearchStartIdx); if (colInfoIdx != -1) { level = GetColInfo(colInfoIdx).OutlineLevel; if (indent) { level++; } else { level--; } level = Math.Max(0, level); level = Math.Min(7, level); colInfoSearchStartIdx = Math.Max(0, colInfoIdx - 1); // -1 just in case this column is collapsed later. } SetColumn(i, null, null, level, null, null); } } /// /// Finds the ColumnInfoRecord /// which contains the specified columnIndex /// /// index of the column (not the index of the ColumnInfoRecord) /// /// null /// if no column info found for the specified column /// public ColumnInfoRecord FindColumnInfo(int columnIndex) { int nInfos = records.Count; for (int i = 0; i < nInfos; i++) { ColumnInfoRecord ci = GetColInfo(i); if (ci.ContainsColumn(columnIndex)) { return ci; } } return null; } private int FindColInfoIdx(int columnIx, int fromColInfoIdx) { if (columnIx < 0) { throw new ArgumentException("column parameter out of range: " + columnIx); } if (fromColInfoIdx < 0) { throw new ArgumentException("fromIdx parameter out of range: " + fromColInfoIdx); } for (int k = fromColInfoIdx; k < records.Count; k++) { ColumnInfoRecord ci = GetColInfo(k); if (ci.ContainsColumn(columnIx)) { return k; } if (ci.FirstColumn > columnIx) { break; } } return -1; } /// /// Gets the max outline level. /// /// The max outline level. public int MaxOutlineLevel { get { int result = 0; int count = records.Count; for (int i = 0; i < count; i++) { ColumnInfoRecord columnInfoRecord = GetColInfo(i); result = Math.Max(columnInfoRecord.OutlineLevel, result); } return result; } } } }