diff --git a/libs/textlayout/KoTextLayoutTableArea.cpp b/libs/textlayout/KoTextLayoutTableArea.cpp index 2980c53903b..ba302bbfa4b 100644 --- a/libs/textlayout/KoTextLayoutTableArea.cpp +++ b/libs/textlayout/KoTextLayoutTableArea.cpp @@ -1,1175 +1,1177 @@ /* This file is part of the KDE project * Copyright (C) 2009 Elvis Stansvik * Copyright (C) 2011 C. Boemann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoTextLayoutTableArea.h" #include "KoTextLayoutCellHelper.h" #include "TableIterator.h" #include "KoPointedAt.h" #include "KoCharAreaInfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "FrameIterator.h" +#include + class Q_DECL_HIDDEN KoTextLayoutTableArea::Private { public: Private() : startOfArea(0) { } QVector > cellAreas; TableIterator *startOfArea; TableIterator *endOfArea; bool lastRowHasSomething; QTextTable *table; int headerRows; qreal headerOffsetX; qreal headerOffsetY; KoTableColumnAndRowStyleManager carsManager; qreal tableWidth; QVector headerRowPositions; // we will only fill those that this area covers QVector rowPositions; // we will only fill those that this area covers QVector columnWidths; QVector columnPositions; bool collapsing; bool totalMisFit; KoTextDocumentLayout *documentLayout; KoTableCellStyle effectiveCellStyle(const QTextTableCell &tableCell); }; KoTableCellStyle KoTextLayoutTableArea::Private::effectiveCellStyle(const QTextTableCell &tableCell) { QTextTableFormat tableFormat = table->format(); KoTableCellStyle cellStyle(tableCell.format().toTableCellFormat()); if (documentLayout->styleManager() && table->format().hasProperty(KoTableStyle::TableTemplate)) { if (KoTextTableTemplate *tableTemplate = documentLayout->styleManager()->tableTemplate(table->format().intProperty(KoTableStyle::TableTemplate))) { //priorities according to ODF 1.2, 16.18 - table:table-template if (tableCell.column() == 0 && tableTemplate->firstColumn() && tableFormat.boolProperty(KoTableStyle::UseFirstColumnStyles)) { cellStyle = *(documentLayout->styleManager()->tableCellStyle(tableTemplate->firstColumn())); return cellStyle; } if (tableCell.column() == (table->columns() - 1) && tableTemplate->lastColumn() && tableFormat.boolProperty(KoTableStyle::UseLastColumnStyles)) { cellStyle = *(documentLayout->styleManager()->tableCellStyle(tableTemplate->lastColumn())); return cellStyle; } if (tableCell.row() == 0 && tableTemplate->firstRow() && tableFormat.boolProperty(KoTableStyle::UseFirstRowStyles)) { cellStyle = *(documentLayout->styleManager()->tableCellStyle(tableTemplate->firstRow())); return cellStyle; } if (tableCell.row() == (table->rows() - 1) && tableTemplate->lastRow() && tableFormat.boolProperty(KoTableStyle::UseLastRowStyles)) { cellStyle = *(documentLayout->styleManager()->tableCellStyle(tableTemplate->lastRow())); return cellStyle; } if (((tableCell.row() + 1) % 2) == 0 && tableTemplate->evenRows() && tableFormat.boolProperty(KoTableStyle::UseBandingRowStyles)) { cellStyle = *(documentLayout->styleManager()->tableCellStyle(tableTemplate->evenRows())); return cellStyle; } if (((tableCell.row() + 1) % 2) != 0 && tableTemplate->oddRows() && tableFormat.boolProperty(KoTableStyle::UseBandingRowStyles)) { cellStyle = *(documentLayout->styleManager()->tableCellStyle(tableTemplate->oddRows())); return cellStyle; } if (((tableCell.column() + 1) % 2) == 0 && tableTemplate->evenColumns() && tableFormat.boolProperty(KoTableStyle::UseBandingColumnStyles)) { cellStyle = *(documentLayout->styleManager()->tableCellStyle(tableTemplate->evenColumns())); return cellStyle; } if (((tableCell.column() + 1) % 2) != 0 && tableTemplate->oddColumns() && tableFormat.boolProperty(KoTableStyle::UseBandingColumnStyles)) { cellStyle = *(documentLayout->styleManager()->tableCellStyle(tableTemplate->oddColumns())); return cellStyle; } if (tableTemplate->body()) { cellStyle = *(documentLayout->styleManager()->tableCellStyle(tableTemplate->body())); } } } return cellStyle; } KoTextLayoutTableArea::KoTextLayoutTableArea(QTextTable *table, KoTextLayoutArea *parent, KoTextDocumentLayout *documentLayout) : KoTextLayoutArea(parent, documentLayout) , d(new Private) { Q_ASSERT(table); Q_ASSERT(parent); d->table = table; d->documentLayout = documentLayout; d->carsManager = KoTableColumnAndRowStyleManager::getManager(table); // Resize geometry vectors for the table. d->rowPositions.resize(table->rows() + 1); d->headerRowPositions.resize(table->rows() + 1); d->cellAreas.resize(table->rows()); for (int row = 0; row < table->rows(); ++row) { d->cellAreas[row].resize(table->columns()); } d->collapsing = d->table->format().boolProperty(KoTableStyle::CollapsingBorders); } KoTextLayoutTableArea::~KoTextLayoutTableArea() { for (int row = d->startOfArea->row; row < d->cellAreas.size(); ++row) { for (int col = 0; col < d->cellAreas[row].size(); ++col) { delete d->cellAreas[row][col]; } } delete d->startOfArea; delete d->endOfArea; delete d; } KoPointedAt KoTextLayoutTableArea::hitTest(const QPointF &point, Qt::HitTestAccuracy accuracy) const { int firstRow = qMax(d->startOfArea->row, d->headerRows); int lastRow = d->endOfArea->row; if (d->lastRowHasSomething == false) { --lastRow; } if (lastRow < d->startOfArea->row) { return KoPointedAt(); // empty } // Test normal cells. if (point.y() > d->rowPositions[firstRow] - 3.0 && point.y() < d->rowPositions[lastRow + 1] + 3.0) { QVector::const_iterator start = d->rowPositions.constBegin() + firstRow; QVector::const_iterator end = d->rowPositions.constBegin() + lastRow + 1; - int row = qLowerBound(start, end, point.y()) - d->rowPositions.constBegin() - 1; - int column = qLowerBound(d->columnPositions, point.x()) - d->columnPositions.constBegin() - 1; + int row = std::lower_bound(start, end, point.y()) - d->rowPositions.constBegin() - 1; + int column = std::lower_bound(d->columnPositions.begin(), d->columnPositions.end(), point.x()) - d->columnPositions.constBegin() - 1; if (point.y() < d->rowPositions[firstRow]) { ++row; } column = qBound(0, column, d->table->columns() - 1); KoPointedAt pointedAt; if (qAbs(d->columnPositions[column] - point.x()) < 3.0) { pointedAt.tableHit = KoPointedAt::ColumnDivider; } else if (qAbs(d->columnPositions[column+1] - point.x()) < 3.0) { pointedAt.tableHit = KoPointedAt::ColumnDivider; ++column; } else if (d->columnPositions[0] < point.x() && point.x() < d->columnPositions[d->table->columns()] && qAbs(d->rowPositions[row] - point.y()) < 3.0) { pointedAt.tableHit = KoPointedAt::RowDivider; } else if (d->columnPositions[0] < point.x() && point.x() < d->columnPositions[d->table->columns()] && qAbs(d->rowPositions[row+1] - point.y()) < 3.0) { pointedAt.tableHit = KoPointedAt::RowDivider; ++row; } else { QTextTableCell cell = d->table->cellAt(row, column); pointedAt = d->cellAreas[cell.row()][cell.column()]->hitTest(point, accuracy); } if (pointedAt.tableHit == KoPointedAt::ColumnDivider) { if (column > 0) { pointedAt.tableLeadSize = d->columnPositions[column] - d->columnPositions[column-1]; } if (column < d->table->columns()) { pointedAt.tableTrailSize = d->columnPositions[column+1] - d->columnPositions[column]; } } else if (pointedAt.tableHit == KoPointedAt::RowDivider) { if (row > 0) { pointedAt.tableLeadSize = d->rowPositions[row] - d->rowPositions[row-1]; } if (row < d->table->rows()) { pointedAt.tableTrailSize = d->rowPositions[row+1] - d->rowPositions[row]; } } pointedAt.table = d->table; pointedAt.tableRowDivider = row; pointedAt.tableColumnDivider = column; pointedAt.tableDividerPos = QPointF(d->columnPositions[column],d->rowPositions[row]); return pointedAt; } // Test header row cells. QPointF headerPoint = point - QPointF(d->headerOffsetX, d->headerOffsetY); if (headerPoint.y() > d->headerRowPositions.first() && headerPoint.y() < d->headerRowPositions[d->headerRows]) { QVector::const_iterator start = d->headerRowPositions.constBegin(); QVector::const_iterator end = d->headerRowPositions.constBegin() + d->headerRows; - int row = qLowerBound(start, end, headerPoint.y()) - d->headerRowPositions.constBegin() - 1; - int column = qLowerBound(d->columnPositions, headerPoint.x()) - d->columnPositions.constBegin() - 1; + int row = std::lower_bound(start, end, headerPoint.y()) - d->headerRowPositions.constBegin() - 1; + int column = std::lower_bound(d->columnPositions.begin(), d->columnPositions.end(), headerPoint.x()) - d->columnPositions.constBegin() - 1; column = qBound(0, column, d->table->columns() - 1); KoPointedAt pointedAt; if (qAbs(d->columnPositions[column] - headerPoint.x()) < 3.0) { pointedAt.tableHit = KoPointedAt::ColumnDivider; } else if (qAbs(d->columnPositions[column+1] - headerPoint.x()) < 3.0) { pointedAt.tableHit = KoPointedAt::ColumnDivider; ++column; } else { QTextTableCell cell = d->table->cellAt(row, column); pointedAt = d->cellAreas[cell.row()][cell.column()]->hitTest(headerPoint, accuracy); } if (pointedAt.tableHit == KoPointedAt::ColumnDivider) { if (column > 0) { pointedAt.tableLeadSize = d->columnPositions[column] - d->columnPositions[column-1]; } if (column < d->table->columns()) { pointedAt.tableTrailSize = d->columnPositions[column+1] - d->columnPositions[column]; } } pointedAt.table = d->table; pointedAt.tableRowDivider = row; pointedAt.tableColumnDivider = column; pointedAt.tableDividerPos = QPointF(d->columnPositions[column],d->rowPositions[row]); return pointedAt; } return KoPointedAt(); } QVector KoTextLayoutTableArea::generateCharAreaInfos() const { QVector result; if (d->startOfArea == 0) { // We have not been layouted yet return result; } int lastRow = d->endOfArea->row; if (d->lastRowHasSomething == false) { --lastRow; } if (lastRow < d->startOfArea->row) { return result; // empty } // TODO: check why paint() does use visitedCells only for non-header rows QSet > visitedCells; const int firstRow = qMax(d->startOfArea->row, d->headerRows); for (int row = 0; row < d->headerRows; ++row) { // TODO: use lambda to shware inner loop with below for (int column = 0; column < d->table->columns(); ++column) { QTextTableCell tableCell = d->table->cellAt(row, column); const int testRow = (row == firstRow ? tableCell.row() : row); if (d->cellAreas[testRow][column] && !visitedCells.contains(QPair(testRow, column))) { const int column = tableCell.column(); result.append(d->cellAreas[testRow][column]->generateCharAreaInfos()); visitedCells.insert(QPair(testRow, column)); } } } for (int row = firstRow; row <= lastRow; ++row) { for (int column = 0; column < d->table->columns(); ++column) { QTextTableCell tableCell = d->table->cellAt(row, column); const int testRow = (row == firstRow ? tableCell.row() : row); if (d->cellAreas[testRow][column] && !visitedCells.contains(QPair(testRow, column))) { const int column = tableCell.column(); result.append(d->cellAreas[testRow][column]->generateCharAreaInfos()); visitedCells.insert(QPair(testRow, column)); } } } return result; } QRectF KoTextLayoutTableArea::selectionBoundingBox(QTextCursor &cursor) const { int lastRow = d->endOfArea->row; if (d->lastRowHasSomething == false) { --lastRow; } if (lastRow < d->startOfArea->row) { return QRectF(); // empty } int firstRow = qMax(d->startOfArea->row, d->headerRows); QTextTableCell startTableCell = d->table->cellAt(cursor.selectionStart()); QTextTableCell endTableCell = d->table->cellAt(cursor.selectionEnd()); if (startTableCell == endTableCell) { if (startTableCell.row() < d->startOfArea->row || startTableCell.row() > lastRow) { return QRectF(); // cell is not in this area } KoTextLayoutArea *area = d->cellAreas[startTableCell.row()][startTableCell.column()]; Q_ASSERT(area); return area->selectionBoundingBox(cursor); } else { int selectionRow; int selectionColumn; int selectionRowSpan; int selectionColumnSpan; cursor.selectedTableCells(&selectionRow, &selectionRowSpan, &selectionColumn, &selectionColumnSpan); qreal top, bottom; if (selectionRow < d->headerRows) { top = d->headerRowPositions[selectionRow] + d->headerOffsetY; } else { top = d->rowPositions[qMin(qMax(firstRow, selectionRow), lastRow)]; } if (selectionRow + selectionRowSpan < d->headerRows) { bottom = d->headerRowPositions[selectionRow + selectionRowSpan] + d->headerOffsetY; } else { bottom = d->rowPositions[d->headerRows] + d->headerOffsetY; if (selectionRow + selectionRowSpan >= firstRow) { bottom = d->rowPositions[qMin(selectionRow + selectionRowSpan, lastRow + 1)]; } } return QRectF(d->columnPositions[selectionColumn], top, d->columnPositions[selectionColumn + selectionColumnSpan] - d->columnPositions[selectionColumn], bottom - top); } } bool KoTextLayoutTableArea::layoutTable(TableIterator *cursor) { d->startOfArea = new TableIterator(cursor); d->headerRows = cursor->headerRows; d->totalMisFit = false; // If table is done we create an empty area and return true if (cursor->row == d->table->rows()) { setBottom(top()); d->endOfArea = new TableIterator(cursor); return true; } layoutColumns(); bool first = cursor->row == 0 && (d->cellAreas[0][0] == 0); if (first) { // are we at the beginning of the table cursor->row = 0; d->rowPositions[0] = top() + d->table->format().topMargin(); d->headerOffsetX = 0; d->headerOffsetY = 0; } else { for (int row = 0; row < d->headerRows; ++row) { // Copy header rows d->headerRowPositions[row] = cursor->headerRowPositions[row]; for (int col = 0; col < d->table->columns(); ++col) { d->cellAreas[row][col] = cursor->headerCellAreas[row][col]; } } if (d->headerRows) { // Also set the position of the border below headers d->headerRowPositions[d->headerRows] = cursor->headerRowPositions[d->headerRows]; } // If headerRows == 0 then the following reduces to: d->rowPositions[cursor->row] = top() d->headerOffsetY = top() - d->headerRowPositions[0]; d->rowPositions[cursor->row] = d->headerRowPositions[d->headerRows] + d->headerOffsetY; // headerOffsetX should also be set d->headerOffsetX = d->columnPositions[0] - cursor->headerPositionX; } bool complete = first; qreal topBorderWidth = 0; qreal bottomBorderWidth = 0; qreal dummyWidth = 0; collectBorderThicknesss(cursor->row - 1, dummyWidth, topBorderWidth); collectBorderThicknesss(cursor->row, topBorderWidth, bottomBorderWidth); do { qreal nextBottomBorderWidth = 0; collectBorderThicknesss(cursor->row+1, bottomBorderWidth, nextBottomBorderWidth); d->lastRowHasSomething = false; complete = layoutRow(cursor, topBorderWidth, bottomBorderWidth); setBottom(d->rowPositions[cursor->row + 1] + bottomBorderWidth); topBorderWidth = bottomBorderWidth; bottomBorderWidth = nextBottomBorderWidth; if (complete) { setVirginPage(false); cursor->row++; } } while (complete && cursor->row < d->table->rows()); if (cursor->row == d->table->rows()) { d->lastRowHasSomething = false; } if (first) { // were we at the beginning of the table for (int row = 0; row < d->headerRows; ++row) { // Copy header rows cursor->headerRowPositions[row] = d->rowPositions[row]; d->headerRowPositions[row] = d->rowPositions[row]; for (int col = 0; col < d->table->columns(); ++col) { cursor->headerCellAreas[row][col] = d->cellAreas[row][col]; } } if (d->headerRows) { // Also set the position of the border below headers cursor->headerRowPositions[d->headerRows] = d->rowPositions[d->headerRows]; d->headerRowPositions[d->headerRows] = d->rowPositions[d->headerRows]; } cursor->headerPositionX = d->columnPositions[0]; if (!virginPage() && d->totalMisFit) { //if we couldn't fit the header rows plus some then don't even try cursor->row = 0; nukeRow(cursor); } } d->endOfArea = new TableIterator(cursor); return complete; } void KoTextLayoutTableArea::layoutColumns() { QTextTableFormat tableFormat = d->table->format(); d->columnPositions.resize(d->table->columns() + 1); d->columnWidths.resize(d->table->columns() + 1); // Table width. d->tableWidth = 0; qreal parentWidth = right() - left(); if (tableFormat.width().rawValue() == 0 || tableFormat.alignment() == Qt::AlignJustify) { // We got a zero width value or alignment is justify, so use 100% of parent. d->tableWidth = parentWidth - tableFormat.leftMargin() - tableFormat.rightMargin(); } else { if (tableFormat.width().type() == QTextLength::FixedLength) { // Fixed length value, so use the raw value directly. d->tableWidth = tableFormat.width().rawValue(); } else if (tableFormat.width().type() == QTextLength::PercentageLength) { // Percentage length value, so use a percentage of parent width. d->tableWidth = tableFormat.width().rawValue() * (parentWidth / 100) - tableFormat.leftMargin() - tableFormat.rightMargin(); } else { // Unknown length type, so use 100% of parent. d->tableWidth = parentWidth - tableFormat.leftMargin() - tableFormat.rightMargin(); } } // Column widths. qreal availableWidth = d->tableWidth; // Width available for columns. QList fixedWidthColumns; // List of fixed width columns. QList relativeWidthColumns; // List of relative width columns. qreal relativeWidthSum = 0; // Sum of relative column width values. int numNonStyleColumns = 0; for (int col = 0; col < d->table->columns(); ++col) { KoTableColumnStyle columnStyle = d->carsManager.columnStyle(col); if (columnStyle.hasProperty(KoTableColumnStyle::RelativeColumnWidth)) { // Relative width specified. Will be handled in the next loop. d->columnWidths[col] = 0.0; relativeWidthColumns.append(col); relativeWidthSum += columnStyle.relativeColumnWidth(); } else if (columnStyle.hasProperty(KoTableColumnStyle::ColumnWidth)) { // Only width specified, so use it. d->columnWidths[col] = columnStyle.columnWidth(); fixedWidthColumns.append(col); availableWidth -= columnStyle.columnWidth(); } else { // Neither width nor relative width specified. d->columnWidths[col] = 0.0; relativeWidthColumns.append(col); // handle it as a relative width column without asking for anything ++numNonStyleColumns; } } // Handle the case that the fixed size columns are larger then the defined table width if (availableWidth < 0.0) { if (tableFormat.width().rawValue() == 0 && fixedWidthColumns.count() > 0) { // If not table width was defined then we need to scale down the fixed size columns so they match // into the width of the table. qreal diff = (-availableWidth) / qreal(fixedWidthColumns.count()); foreach(int col, fixedWidthColumns) { d->columnWidths[col] = qMax(qreal(0.0), d->columnWidths[col] - diff); } } availableWidth = 0.0; } // Calculate width to those columns that don't actually request it qreal widthForNonWidthColumn = ((1.0 - qMin(relativeWidthSum, 1.0)) * availableWidth); availableWidth -= widthForNonWidthColumn; // might as well do this calc before dividing by numNonStyleColumns if (numNonStyleColumns > 0 && widthForNonWidthColumn > 0.0) { widthForNonWidthColumn /= numNonStyleColumns; } // Relative column widths have now been summed up and can be distributed. foreach (int col, relativeWidthColumns) { KoTableColumnStyle columnStyle = d->carsManager.columnStyle(col); if (columnStyle.hasProperty(KoTableColumnStyle::RelativeColumnWidth) || columnStyle.hasProperty(KoTableColumnStyle::ColumnWidth)) { d->columnWidths[col] = qMax(columnStyle.relativeColumnWidth() * availableWidth / relativeWidthSum, 0.0); } else { d->columnWidths[col] = widthForNonWidthColumn; } } // Column positions. qreal columnPosition = left(); qreal columnOffset = tableFormat.leftMargin(); if (tableFormat.alignment() == Qt::AlignRight) { // Table is right-aligned, so add all of the remaining space. columnOffset += parentWidth - d->tableWidth; } if (tableFormat.alignment() == Qt::AlignHCenter) { // Table is centered, so add half of the remaining space. columnOffset += (parentWidth - d->tableWidth) / 2; } for (int col = 0; col < d->columnPositions.size(); ++col) { d->columnPositions[col] = columnPosition + columnOffset; // Increment by this column's width. columnPosition += d->columnWidths[col]; } // Borders can be outside of the cell (outer-borders) in which case it's need // to take them into account to not cut content off. qreal leftBorder = 0.0; qreal rightBorder = 0.0; for (int row = 0; row < d->table->rows(); ++row) { QTextTableCell leftCell = d->table->cellAt(row, 0); KoTableCellStyle leftCellStyle = d->effectiveCellStyle(leftCell); leftBorder = qMax(leftBorder, leftCellStyle.leftOuterBorderWidth()); QTextTableCell rightCell = d->table->cellAt(row, d->table->columns() - 1); KoTableCellStyle rightCellStyle = d->effectiveCellStyle(rightCell); rightBorder = qMax(rightBorder, rightCellStyle.rightOuterBorderWidth()); } expandBoundingLeft(d->columnPositions[0] - leftBorder); expandBoundingRight(d->columnPositions[d->table->columns()] + rightBorder + leftBorder); } void KoTextLayoutTableArea::collectBorderThicknesss(int row, qreal &topBorderWidth, qreal &bottomBorderWidth) { int col = 0; if (d->collapsing && row >= 0 && row < d->table->rows()) { // let's collect the border info while (col < d->table->columns()) { QTextTableCell cell = d->table->cellAt(row, col); if (row == cell.row() + cell.rowSpan() - 1) { /* * This cell ends vertically in this row, and hence should * contribute to the bottom border. */ KoTableCellStyle cellStyle = d->effectiveCellStyle(cell); topBorderWidth = qMax(cellStyle.topBorderWidth(), topBorderWidth); bottomBorderWidth = qMax(cellStyle.bottomBorderWidth(), bottomBorderWidth); } col += cell.columnSpan(); // Skip across column spans. } } } void KoTextLayoutTableArea::nukeRow(TableIterator *cursor) { for (int column = 0; column < d->table->columns(); ++column) { delete d->cellAreas[cursor->row][column]; d->cellAreas[cursor->row][column] = 0; delete cursor->frameIterators[column]; cursor->frameIterators[column] = 0; } d->lastRowHasSomething = false; } bool KoTextLayoutTableArea::layoutRow(TableIterator *cursor, qreal topBorderWidth, qreal bottomBorderWidth) { int row = cursor->row; Q_ASSERT(row >= 0); Q_ASSERT(row < d->table->rows()); /* * Implementation Note: * * An undocumented behavior of QTextTable::cellAt is that requesting a * cell that is covered by a spanning cell will return the cell that * spans over the requested cell. Example: * * +------------+------------+ * | | | * | | | * | +------------+ * | | | * | | | * +------------+------------+ * * table.cellAt(1, 0).row() // Will return 0. * * In the code below, we rely on this behavior to determine wheather * a cell "vertically" ends in the current row, as those are the only * cells that should contribute to the row height. */ KoTableRowStyle rowStyle = d->carsManager.rowStyle(row); qreal rowHeight = rowStyle.rowHeight(); bool rowHasExactHeight = rowStyle.hasProperty(KoTableRowStyle::RowHeight); qreal rowBottom; if (rowHasExactHeight) { rowBottom = d->rowPositions[row] + rowHeight; } else { rowBottom = d->rowPositions[row] + rowStyle.minimumRowHeight(); } if (rowBottom > maximumAllowedBottom()) { d->rowPositions[row+1] = d->rowPositions[row]; if (cursor->row > d->startOfArea->row) { cursor->row--; layoutMergedCellsNotEnding(cursor, topBorderWidth, bottomBorderWidth, rowBottom); cursor->row++; } return false; // we can't honour minimum or fixed height so don't even try } bool allCellsFullyDone = true; bool anyCellTried = false; bool noCellsFitted = true; int col = 0; while (col < d->table->columns()) { // Get the cell format. QTextTableCell cell = d->table->cellAt(row, col); if (row == cell.row() + cell.rowSpan() - 1) { /* * This cell ends vertically in this row, and hence should * contribute to the row height. */ bool ignoreMisFittingCell = false; KoTableCellStyle cellStyle = d->effectiveCellStyle(cell); anyCellTried = true; qreal maxBottom = maximumAllowedBottom(); qreal requiredRowHeight = cellStyle.bottomPadding() + cellStyle.bottomPadding(); if (rowHasExactHeight) { maxBottom = qMin(d->rowPositions[row] + rowHeight, maxBottom); } maxBottom -= cellStyle.bottomPadding(); qreal areaTop = d->rowPositions[qMax(cell.row(), d->startOfArea->row)] + cellStyle.topPadding(); if (d->collapsing) { areaTop += topBorderWidth; maxBottom -= bottomBorderWidth; requiredRowHeight += bottomBorderWidth + topBorderWidth; } else { areaTop += cellStyle.topBorderWidth(); maxBottom -= cellStyle.bottomBorderWidth(); requiredRowHeight += cellStyle.bottomBorderWidth() + cellStyle.topBorderWidth(); } if (rowHasExactHeight && (rowHeight < requiredRowHeight)) { ignoreMisFittingCell = true; } if (maxBottom < areaTop && !ignoreMisFittingCell) { d->rowPositions[row+1] = d->rowPositions[row]; nukeRow(cursor); if (cursor->row > d->startOfArea->row) { cursor->row--; layoutMergedCellsNotEnding(cursor, topBorderWidth, bottomBorderWidth, rowBottom); cursor->row++; } return false; // we can't honour the borders so give up doing row } KoTextLayoutArea *cellArea = new KoTextLayoutArea(this, documentLayout()); d->cellAreas[cell.row()][cell.column()] = cellArea; qreal left = d->columnPositions[col] + cellStyle.leftPadding() + cellStyle.leftInnerBorderWidth(); qreal right = qMax(left, d->columnPositions[col+cell.columnSpan()] - cellStyle.rightPadding() - cellStyle.rightInnerBorderWidth()); cellArea->setReferenceRect( left, right, areaTop, maxBottom); cellArea->setVirginPage(virginPage()); cellArea->setLayoutEnvironmentResctictions(true, true); FrameIterator *cellCursor = cursor->frameIterator(col); bool cellFully = cellArea->layout(cellCursor); allCellsFullyDone = allCellsFullyDone && (cellFully || rowHasExactHeight); noCellsFitted = noCellsFitted && (cellArea->top() >= cellArea->bottom()) && !ignoreMisFittingCell; if (!rowHasExactHeight) { /* * Now we know how much height this cell contributes to the row, * and can determine wheather the row height will grow. */ if (d->collapsing) { rowBottom = qMax(cellArea->bottom() + cellStyle.bottomPadding(), rowBottom); } else { rowBottom = qMax(cellArea->bottom() + cellStyle.bottomPadding() + cellStyle.bottomBorderWidth(), rowBottom); } rowBottom = qMax(rowBottom, documentLayout()->maxYOfAnchoredObstructions(cell.firstCursorPosition().position(), cell.lastCursorPosition().position())); } d->lastRowHasSomething = true; // last row contains something (even if empty) } col += cell.columnSpan(); // Skip across column spans. } if (allCellsFullyDone) { for (col = 0; col < d->table->columns(); ++col) { QTextTableCell cell = d->table->cellAt(row, col); if (row == cell.row() + cell.rowSpan() - 1) { delete cursor->frameIterators[col]; cursor->frameIterators[col] = 0; } } } if (noCellsFitted && row <= d->headerRows) { d->totalMisFit = true; } if (anyCellTried && noCellsFitted && !rowHasExactHeight && !allCellsFullyDone) { d->rowPositions[row+1] = d->rowPositions[row]; nukeRow(cursor); if (cursor->row > d->startOfArea->row) { cursor->row--; layoutMergedCellsNotEnding(cursor, topBorderWidth, bottomBorderWidth, rowBottom); cursor->row++; } return false; // we can't honour the anything inside so give up doing row } if (!allCellsFullyDone) { layoutMergedCellsNotEnding(cursor, topBorderWidth, bottomBorderWidth, rowBottom); } else { // Cells all ended naturally, so we can now do vertical alignment // Stop! Other odf implementors also only do it if all cells are fully done col = 0; while (col < d->table->columns()) { QTextTableCell cell = d->table->cellAt(row, col); if (row == cell.row() + cell.rowSpan() - 1) { // cell ended in this row KoTextLayoutArea *cellArea = d->cellAreas[cell.row()][cell.column()]; KoTableCellStyle cellStyle = d->effectiveCellStyle(cell); if (cellStyle.alignment() & Qt::AlignBottom) { if (true /*FIXME test no page based shapes interfering*/) { cellArea->setVerticalAlignOffset(rowBottom - cellArea->bottom()); } } if (cellStyle.alignment() & Qt::AlignVCenter) { if (true /*FIXME test no page based shapes interfering*/) { cellArea->setVerticalAlignOffset((rowBottom - cellArea->bottom()) / 2); } } } col += cell.columnSpan(); // Skip across column spans. } } // Adjust Y position of NEXT row. // This is nice since the outside layout routine relies on the next row having a correct y position // the first row y position is set in layout() d->rowPositions[row+1] = rowBottom; return allCellsFullyDone; } bool KoTextLayoutTableArea::layoutMergedCellsNotEnding(TableIterator *cursor, qreal topBorderWidth, qreal bottomBorderWidth, qreal rowBottom) { Q_UNUSED(topBorderWidth) Q_UNUSED(bottomBorderWidth) // Let's make sure all merged cells in this row, that don't end in this row get's a layout int row = cursor->row; int col = 0; while (col < d->table->columns()) { QTextTableCell cell = d->table->cellAt(row, col); if (row != cell.row() + cell.rowSpan() - 1) { // TODO do all of the following like in layoutRow() KoTableCellStyle cellStyle = d->effectiveCellStyle(cell); KoTextLayoutArea *cellArea = new KoTextLayoutArea(this, documentLayout()); d->cellAreas[cell.row()][cell.column()] = cellArea; qreal left = d->columnPositions[col] + cellStyle.leftPadding() + cellStyle.leftInnerBorderWidth(); qreal right = qMax(left, d->columnPositions[col+cell.columnSpan()] - cellStyle.rightPadding() - cellStyle.rightInnerBorderWidth()); cellArea->setReferenceRect( left, right, d->rowPositions[qMax(cell.row(), d->startOfArea->row)] + cellStyle.topPadding() + cellStyle.topBorderWidth(), rowBottom - cellStyle.bottomPadding() - cellStyle.bottomBorderWidth()); cellArea->setVirginPage(virginPage()); cellArea->setLayoutEnvironmentResctictions(true, true); FrameIterator *cellCursor = cursor->frameIterator(col); cellArea->layout(cellCursor); if (cellArea->top() < cellArea->bottom() && row == d->headerRows) { d->totalMisFit = false; } } col += cell.columnSpan(); // Skip across column spans. } return true; } void KoTextLayoutTableArea::paint(QPainter *painter, const KoTextDocumentLayout::PaintContext &context) { if (d->startOfArea == 0) // We have not been layouted yet return; int lastRow = d->endOfArea->row; if (d->lastRowHasSomething == false) { --lastRow; } if (lastRow < d->startOfArea->row) { return; // empty } int firstRow = qMax(d->startOfArea->row, d->headerRows); // Draw table background qreal topY = d->headerRows ? d->rowPositions[0] : d->rowPositions[firstRow]; QRectF tableRect(d->columnPositions[0], topY, d->tableWidth, d->headerRowPositions[d->headerRows] - d->headerRowPositions[0] + d->rowPositions[lastRow+1] - d->rowPositions[firstRow]); painter->fillRect(tableRect, d->table->format().background()); KoTextDocumentLayout::PaintContext cellContext = context; QColor tableBackground = context.background; if (d->table->format().hasProperty(QTextFormat::BackgroundBrush)) { tableBackground = d->table->format().background().color(); } // Draw header row backgrounds for (int row = 0; row < d->headerRows; ++row) { QRectF rowRect(d->columnPositions[0], d->headerRowPositions[row], d->tableWidth, d->headerRowPositions[row+1] - d->headerRowPositions[row]); KoTableRowStyle rowStyle = d->carsManager.rowStyle(row); rowRect.translate(0, d->headerOffsetY); painter->fillRect(rowRect, rowStyle.background()); } // Draw plain row backgrounds for (int row = firstRow; row <= lastRow; ++row) { QRectF rowRect(d->columnPositions[0], d->rowPositions[row], d->tableWidth, d->rowPositions[row+1] - d->rowPositions[row]); KoTableRowStyle rowStyle = d->carsManager.rowStyle(row); painter->fillRect(rowRect, rowStyle.background()); } QSet > visitedCells; // Draw cell backgrounds and contents. for (int row = firstRow; row <= lastRow; ++row) { for (int column = 0; column < d->table->columns(); ++column) { QTextTableCell tableCell = d->table->cellAt(row, column); int testRow = (row == firstRow ? tableCell.row() : row); if (d->cellAreas[testRow][column] && !visitedCells.contains(QPair(testRow, column))) { cellContext.background = tableBackground; QBrush bgBrush = d->effectiveCellStyle(tableCell).background(); if (bgBrush != Qt::NoBrush) { cellContext.background = bgBrush.color(); } paintCell(painter, cellContext, tableCell, d->cellAreas[testRow][column]); visitedCells.insert(QPair(testRow, column)); } } } painter->translate(0, d->headerOffsetY); QVector accuBlankBorders; bool hasAntialiasing = painter->testRenderHint(QPainter::Antialiasing); // Draw header row cell backgrounds and contents. for (int row = 0; row < d->headerRows; ++row) { for (int column = 0; column < d->table->columns(); ++column) { QTextTableCell tableCell = d->table->cellAt(row, column); int testRow = row == firstRow ? tableCell.row() : row; if (d->cellAreas[testRow][column]) { cellContext.background = tableBackground; QBrush bgBrush = d->effectiveCellStyle(tableCell).background(); if (bgBrush != Qt::NoBrush) { cellContext.background = bgBrush.color(); } paintCell(painter, cellContext, tableCell, d->cellAreas[testRow][column]); } } } // Draw header row cell borders.(need to be second step so neibor cells don't overwrite) for (int row = 0; row < d->headerRows; ++row) { for (int column = 0; column < d->table->columns(); ++column) { QTextTableCell tableCell = d->table->cellAt(row, column); int testRow = row == firstRow ? tableCell.row() : row; if (d->cellAreas[testRow][column]) { painter->setRenderHint(QPainter::Antialiasing, true); paintCellBorders(painter, context, tableCell, false, lastRow, &accuBlankBorders); painter->setRenderHint(QPainter::Antialiasing, hasAntialiasing); } } } for (int i = 0; i < accuBlankBorders.size(); ++i) { accuBlankBorders[i].translate(0, d->headerOffsetY); } painter->translate(0, -d->headerOffsetY); // Draw cell borders. bool topRow = !d->headerRows && firstRow != 0; // are we top row in this area painter->setRenderHint(QPainter::Antialiasing, true); visitedCells.clear(); for (int row = firstRow; row <= lastRow; ++row) { for (int column = 0; column < d->table->columns(); ++column) { QTextTableCell tableCell = d->table->cellAt(row, column); int testRow = row == firstRow ? tableCell.row() : row; if (d->cellAreas[testRow][column] && !visitedCells.contains(QPair(testRow, column))) { paintCellBorders(painter, context, tableCell, topRow, lastRow, &accuBlankBorders); visitedCells.insert(QPair(testRow, column)); } } topRow = false; } painter->setRenderHint(QPainter::Antialiasing, hasAntialiasing); if (context.showTableBorders) { QPen pen(painter->pen()); painter->setPen(QPen(QColor(0,0,0,96), 0)); painter->drawLines(accuBlankBorders); painter->setPen(pen); } } void KoTextLayoutTableArea::paintCell(QPainter *painter, const KoTextDocumentLayout::PaintContext &context, const QTextTableCell &tableCell, KoTextLayoutArea *frameArea) { int row = tableCell.row(); int column = tableCell.column(); // This is an actual cell we want to draw, and not a covered one. QRectF bRect(cellBoundingRect(tableCell)); painter->save(); painter->setClipRect(bRect, Qt::IntersectClip); // Possibly paint the background of the cell QBrush background(d->effectiveCellStyle(tableCell).background()); if (background != Qt::NoBrush) { painter->fillRect(bRect, background); } // Possibly paint the selection of the entire cell if (context.showSelections) { foreach(const QAbstractTextDocumentLayout::Selection & selection, context.textContext.selections) { QTextTableCell startTableCell = d->table->cellAt(selection.cursor.selectionStart()); QTextTableCell endTableCell = d->table->cellAt(selection.cursor.selectionEnd()); if (startTableCell.isValid() && startTableCell != endTableCell) { int selectionRow; int selectionColumn; int selectionRowSpan; int selectionColumnSpan; selection.cursor.selectedTableCells(&selectionRow, &selectionRowSpan, &selectionColumn, &selectionColumnSpan); if (row >= selectionRow && column>=selectionColumn && row < selectionRow + selectionRowSpan && column < selectionColumn + selectionColumnSpan) { painter->fillRect(bRect, selection.format.background()); } } else if (selection.cursor.selectionStart() < d->table->firstPosition() && selection.cursor.selectionEnd() > d->table->lastPosition()) { painter->fillRect(bRect, selection.format.background()); } } } if (row < d->headerRows) { painter->translate(d->headerOffsetX, 0); } // Paint the content of the cellArea frameArea->paint(painter, context); painter->restore(); } void KoTextLayoutTableArea::paintCellBorders(QPainter *painter, const KoTextDocumentLayout::PaintContext &context, const QTextTableCell &tableCell, bool topRow, int lastRow, QVector *accuBlankBorders) { Q_UNUSED(context); int row = tableCell.row(); int column = tableCell.column(); // This is an actual cell we want to draw, and not a covered one. KoTableCellStyle cellStyle = d->effectiveCellStyle(tableCell); KoTextLayoutCellHelper cellStyleHelper(cellStyle); QRectF bRect = cellBoundingRect(tableCell); if (d->collapsing) { // First the horizontal borders if (row == 0) { cellStyleHelper.drawTopHorizontalBorder(*painter, bRect.x(), bRect.y(), bRect.width(), accuBlankBorders); } if (topRow && row != 0) { // in collapsing mode we need to also paint the top border of the area int c = column; while (c < column + tableCell.columnSpan()) { QTextTableCell tableCellAbove = d->table->cellAt(row - 1, c); QRectF aboveBRect = cellBoundingRect(tableCellAbove); qreal x = qMax(bRect.x(), aboveBRect.x()); qreal x2 = qMin(bRect.right(), aboveBRect.right()); KoTableCellStyle cellAboveStyle = d->effectiveCellStyle(tableCellAbove); KoTextLayoutCellHelper cellAboveStyleHelper(cellAboveStyle); cellAboveStyleHelper.drawSharedHorizontalBorder(*painter, cellStyle, x, bRect.y(), x2 - x, accuBlankBorders); c = tableCellAbove.column() + tableCellAbove.columnSpan(); } } if (row + tableCell.rowSpan() == d->table->rows()) { // we hit the bottom of the table so just draw the bottom border cellStyleHelper.drawBottomHorizontalBorder(*painter, bRect.x(), bRect.bottom(), bRect.width(), accuBlankBorders); } else { int c = column; while (c < column + tableCell.columnSpan()) { QTextTableCell tableCellBelow = d->table->cellAt(row == d->headerRows - 1 ? d->startOfArea->row : row + tableCell.rowSpan(), c); QRectF belowBRect = cellBoundingRect(tableCellBelow); qreal x = qMax(bRect.x(), belowBRect.x()); qreal x2 = qMin(bRect.right(), belowBRect.right()); KoTableCellStyle cellBelowStyle = d->effectiveCellStyle(tableCellBelow); cellStyleHelper.drawSharedHorizontalBorder(*painter, cellBelowStyle, x, bRect.bottom(), x2 - x, accuBlankBorders); c = tableCellBelow.column() + tableCellBelow.columnSpan(); } } // And then the same treatment for vertical borders if (column == 0) { cellStyleHelper.drawLeftmostVerticalBorder(*painter, bRect.x(), bRect.y(), bRect.height() + cellStyle.bottomOuterBorderWidth(), accuBlankBorders); } if (column + tableCell.columnSpan() == d->table->columns()) { // we hit the rightmost edge of the table so draw the rightmost border cellStyleHelper.drawRightmostVerticalBorder(*painter, bRect.right(), bRect.y(), bRect.height() + cellStyle.bottomOuterBorderWidth(), accuBlankBorders); } else { // we have cells to the right so draw sharedborders int r = row; while (r < row + tableCell.rowSpan() && r <= lastRow) { QTextTableCell tableCellRight = d->table->cellAt(r, column + tableCell.columnSpan()); KoTableCellStyle cellRightStyle = d->effectiveCellStyle(tableCellRight); QRectF rightBRect = cellBoundingRect(tableCellRight); qreal y = qMax(bRect.y(), rightBRect.y()); qreal y2 = qMin(bRect.bottom() + cellStyle.bottomOuterBorderWidth(), rightBRect.bottom() + cellRightStyle.bottomOuterBorderWidth()); cellStyleHelper.drawSharedVerticalBorder(*painter, cellRightStyle, bRect.right(), y, y2-y, accuBlankBorders); r = tableCellRight.row() + tableCellRight.rowSpan(); } } // Paint diagonal borders for current cell cellStyleHelper.paintDiagonalBorders(*painter, bRect); } else { // separating border model cellStyleHelper.paintBorders(*painter, bRect, accuBlankBorders); } } QRectF KoTextLayoutTableArea::cellBoundingRect(const QTextTableCell &cell) const { int row = cell.row(); int rowSpan = cell.rowSpan(); const int column = cell.column(); const int columnSpan = cell.columnSpan(); const qreal width = d->columnPositions[column + columnSpan] - d->columnPositions[column]; if (row >= d->headerRows) { int lastRow = d->endOfArea->row; if (d->lastRowHasSomething == false) { --lastRow; } if (lastRow < d->startOfArea->row) { return QRectF(); // empty } // Limit cell to within the area if (row < d->startOfArea->row) { rowSpan -= d->startOfArea->row - row; row += d->startOfArea->row - row; } if (row + rowSpan - 1 > lastRow) { rowSpan = lastRow - row + 1; } const qreal height = d->rowPositions[row + rowSpan] - d->rowPositions[row]; return QRectF(d->columnPositions[column], d->rowPositions[row], width, height); } else { return QRectF(d->columnPositions[column], d->headerRowPositions[row], width, d->headerRowPositions[row + rowSpan] - d->headerRowPositions[row]); } } diff --git a/sheets/PointStorage.h b/sheets/PointStorage.h index dd0bdedf39d..1318d98573d 100644 --- a/sheets/PointStorage.h +++ b/sheets/PointStorage.h @@ -1,880 +1,882 @@ /* This file is part of the KDE project Copyright 2007 Stefan Nikolaus This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CALLIGRA_SHEETS_POINT_STORAGE #define CALLIGRA_SHEETS_POINT_STORAGE #include #include #include #include "Region.h" #include "calligra_sheets_limits.h" +#include + // #define KSPREAD_POINT_STORAGE_HASH namespace Calligra { namespace Sheets { /** * \ingroup Storage * A custom pointwise storage. * Based on a compressed sparse matrix data structure. * Usable for any kind of data attached to 2D coordinates. * * Only non-default data with its coordinate is stored. Hence, the storage * has a small memory footprint nearly regardless of the data's location. * Each empty row before a location occupy an integer, which is not the case * for columns. Iterating over the data becomes fast compared to dense * matrix/array, where each location has to be traversed irrespective of * default or non-default data. * * The actual data is stored in the list m_data. It is grouped by rows in * ascending order. The rows' beginnings and ends are stored in the list * m_rows. Its index corresponds to the row index. The values denote the * starting index of a row in m_data. The row's end is determined by * the starting position of the next row. The entries in each row are ordered * by column. The corresponding column indices are stored in m_cols. Hence, * m_cols has the same amount of entries as m_data. * * \author Stefan Nikolaus * * \note If you fill the storage, do it row-wise. That's more performant. * \note For data assigned to rectangular regions use RectStorage. * \note It's QVector based. To boost performance a lot, declare the stored * data type as movable. */ template class PointStorage { friend class PointStorageBenchmark; friend class PointStorageTest; public: /** * Constructor. * Creates an empty storage. Actually, does nothing. */ PointStorage() {} /** * Destructor. */ ~PointStorage() {} /** * Clears the storage. */ void clear() { m_cols.clear(); m_rows.clear(); m_data.clear(); } /** * Returns the number of items in the storage. * Usable to iterate over all non-default data. * \return number of items * \see col() * \see row() * \see data() */ int count() const { return m_data.count(); } /** * Inserts \p data at \p col , \p row . * \return the overridden data (default data, if no overwrite) */ T insert(int col, int row, const T& data) { Q_ASSERT(1 <= col && col <= KS_colMax); Q_ASSERT(1 <= row && row <= KS_rowMax); // row's missing? if (row > m_rows.count()) { // insert missing rows m_rows.insert(m_rows.count(), row - m_rows.count(), m_data.count()); // append the actual data #ifdef KSPREAD_POINT_STORAGE_HASH m_data.append(*m_usedData.insert(data)); #else m_data.append(data); #endif // append the column index m_cols.append(col); } // the row exists else { const QVector::const_iterator cstart(m_cols.begin() + m_rows.value(row - 1)); const QVector::const_iterator cend((row < m_rows.count()) ? (m_cols.begin() + m_rows.value(row)) : m_cols.end()); - const QVector::const_iterator cit = qLowerBound(cstart, cend, col); + const QVector::const_iterator cit = std::lower_bound(cstart, cend, col); // column's missing? if (cit == cend || *cit != col) { // determine the index where the data and column has to be inserted const int index = m_rows.value(row - 1) + (cit - cstart); // insert the actual data #ifdef KSPREAD_POINT_STORAGE_HASH m_data.insert(index, *m_usedData.insert(data)); #else m_data.insert(index, data); #endif // insert the column index m_cols.insert(index, col); // adjust the offsets of the following rows for (int r = row; r < m_rows.count(); ++r) ++m_rows[r]; } // column exists else { const int index = m_rows.value(row - 1) + (cit - cstart); const T oldData = m_data[ index ]; #ifdef KSPREAD_POINT_STORAGE_HASH m_data[ index ] = *m_usedData.insert(data); #else m_data[ index ] = data; #endif return oldData; } } squeezeRows(); return T(); } /** * Looks up the data at \p col , \p row . If no data was found returns a * default object. * \return the data at the given coordinate */ T lookup(int col, int row, const T& defaultVal = T()) const { Q_ASSERT(1 <= col && col <= KS_colMax); Q_ASSERT(1 <= row && row <= KS_rowMax); // is the row not present? if (row > m_rows.count()) return defaultVal; const QVector::const_iterator cstart(m_cols.begin() + m_rows.value(row - 1)); const QVector::const_iterator cend((row < m_rows.count()) ? (m_cols.begin() + m_rows.value(row)) : m_cols.end()); const QVector::const_iterator cit = qBinaryFind(cstart, cend, col); // is the col not present? if (cit == cend) return defaultVal; return m_data.value(m_rows.value(row - 1) + (cit - cstart)); } /** * Removes data at \p col , \p row . * \return the removed data (default data, if none) */ T take(int col, int row, const T& defaultVal = T()) { Q_ASSERT(1 <= col && col <= KS_colMax); Q_ASSERT(1 <= row && row <= KS_rowMax); // row's missing? if (row > m_rows.count()) return defaultVal; const int rowStart = (row - 1 < m_rows.count()) ? m_rows.value(row - 1) : m_data.count(); const int rowLength = (row < m_rows.count()) ? m_rows.value(row) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); QVector::const_iterator cit = qBinaryFind(cols, col); // column's missing? if (cit == cols.constEnd()) return defaultVal; const int index = rowStart + (cit - cols.constBegin()); // save the old data const T oldData = m_data[ index ]; // remove the actual data m_data.remove(index); // remove the column index m_cols.remove(index); // adjust the offsets of the following rows for (int r = row; r < m_rows.count(); ++r) --m_rows[r]; squeezeRows(); return oldData; } /** * Insert \p number columns at \p position . * \return the data, that became out of range (shifted over the end) */ QVector< QPair > insertColumns(int position, int number) { Q_ASSERT(1 <= position && position <= KS_colMax); QVector< QPair > oldData; for (int row = m_rows.count(); row >= 1; --row) { const int rowStart = m_rows.value(row - 1); const int rowLength = (row < m_rows.count()) ? m_rows.value(row) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); for (int col = cols.count(); col >= 0; --col) { if (cols.value(col) + number > KS_colMax) { oldData.append(qMakePair(QPoint(cols.value(col), row), m_data.value(rowStart + col))); m_cols.remove(rowStart + col); m_data.remove(rowStart + col); // adjust the offsets of the following rows for (int r = row; r < m_rows.count(); ++r) --m_rows[r]; } else if (cols.value(col) >= position) m_cols[rowStart + col] += number; } } squeezeRows(); return oldData; } /** * Removes \p number columns at \p position . * \return the removed data */ QVector< QPair > removeColumns(int position, int number) { Q_ASSERT(1 <= position && position <= KS_colMax); QVector< QPair > oldData; for (int row = m_rows.count(); row >= 1; --row) { const int rowStart = m_rows.value(row - 1); const int rowLength = (row < m_rows.count()) ? m_rows.value(row) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); for (int col = cols.count() - 1; col >= 0; --col) { if (cols.value(col) >= position) { if (cols.value(col) < position + number) { oldData.append(qMakePair(QPoint(cols.value(col), row), m_data.value(rowStart + col))); m_cols.remove(rowStart + col); m_data.remove(rowStart + col); for (int r = row; r < m_rows.count(); ++r) --m_rows[r]; } else m_cols[rowStart + col] -= number; } } } squeezeRows(); return oldData; } /** * Insert \p number rows at \p position . * \return the data, that became out of range (shifted over the end) */ QVector< QPair > insertRows(int position, int number) { Q_ASSERT(1 <= position && position <= KS_rowMax); // row's missing? if (position > m_rows.count()) return QVector< QPair >(); QVector< QPair > oldData; int dataCount = 0; int rowCount = 0; // save the old data for (int row = KS_rowMax - number + 1; row <= m_rows.count() && row <= KS_rowMax; ++row) { const QVector::const_iterator cstart(m_cols.begin() + m_rows.value(row - 1)); const QVector::const_iterator cend((row < m_rows.count()) ? (m_cols.begin() + m_rows.value(row)) : m_cols.end()); for (QVector::const_iterator cit = cstart; cit != cend; ++cit) oldData.append(qMakePair(QPoint(*cit, row), m_data.value(cit - m_cols.constBegin()))); dataCount += (cend - cstart); ++rowCount; } // remove the out of range data while (dataCount-- > 0) { m_data.remove(m_data.count() - 1); m_cols.remove(m_cols.count() - 1); } while (rowCount-- > 0) m_rows.remove(m_rows.count() - 1); // insert the new rows const int index = m_rows.value(position - 1); for (int r = 0; r < number; ++r) m_rows.insert(position, index); squeezeRows(); return oldData; } /** * Removes \p number rows at \p position . * \return the removed data */ QVector< QPair > removeRows(int position, int number) { Q_ASSERT(1 <= position && position <= KS_rowMax); // row's missing? if (position > m_rows.count()) return QVector< QPair >(); QVector< QPair > oldData; int dataCount = 0; int rowCount = 0; // save the old data for (int row = position; row <= m_rows.count() && row <= position + number - 1; ++row) { const int rowStart = m_rows.value(row - 1); const int rowLength = (row < m_rows.count()) ? m_rows.value(row) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); const QVector data = m_data.mid(rowStart, rowLength); for (int col = 0; col < cols.count(); ++col) oldData.append(qMakePair(QPoint(cols.value(col), row), data.value(col))); dataCount += data.count(); ++rowCount; } // adjust the offsets of the following rows for (int r = position + number - 1; r < m_rows.count(); ++r) m_rows[r] -= dataCount; // remove the out of range data while (dataCount-- > 0) { m_data.remove(m_rows.value(position - 1)); m_cols.remove(m_rows.value(position - 1)); } while (rowCount-- > 0) m_rows.remove(position - 1); squeezeRows(); return oldData; } /** * Shifts the data right of \p rect to the left by the width of \p rect . * The data formerly contained in \p rect becomes overridden. * \return the removed data */ QVector< QPair > removeShiftLeft(const QRect& rect) { Q_ASSERT(1 <= rect.left() && rect.left() <= KS_colMax); QVector< QPair > oldData; for (int row = qMin(rect.bottom(), m_rows.count()); row >= rect.top(); --row) { const int rowStart = m_rows.value(row - 1); const int rowLength = (row < m_rows.count()) ? m_rows.value(row) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); for (int col = cols.count() - 1; col >= 0; --col) { if (cols.value(col) >= rect.left()) { if (cols.value(col) <= rect.right()) { oldData.append(qMakePair(QPoint(cols.value(col), row), m_data.value(rowStart + col))); m_cols.remove(rowStart + col); m_data.remove(rowStart + col); for (int r = row; r < m_rows.count(); ++r) --m_rows[r]; } else m_cols[rowStart + col] -= rect.width(); } } } squeezeRows(); return oldData; } /** * Shifts the data in and right of \p rect to the right by the width of \p rect . * \return the data, that became out of range (shifted over the end) */ QVector< QPair > insertShiftRight(const QRect& rect) { Q_ASSERT(1 <= rect.left() && rect.left() <= KS_colMax); QVector< QPair > oldData; for (int row = rect.top(); row <= rect.bottom() && row <= m_rows.count(); ++row) { const int rowStart = m_rows.value(row - 1); const int rowLength = (row < m_rows.count()) ? m_rows.value(row) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); for (int col = cols.count(); col >= 0; --col) { if (cols.value(col) + rect.width() > KS_colMax) { oldData.append(qMakePair(QPoint(cols.value(col), row), m_data.value(rowStart + col))); m_cols.remove(rowStart + col); m_data.remove(rowStart + col); // adjust the offsets of the following rows for (int r = row; r < m_rows.count(); ++r) --m_rows[r]; } else if (cols.value(col) >= rect.left()) m_cols[rowStart + col] += rect.width(); } } squeezeRows(); return oldData; } /** * Shifts the data below \p rect to the top by the height of \p rect . * The data formerly contained in \p rect becomes overridden. * \return the removed data */ QVector< QPair > removeShiftUp(const QRect& rect) { Q_ASSERT(1 <= rect.top() && rect.top() <= KS_rowMax); // row's missing? if (rect.top() > m_rows.count()) return QVector< QPair >(); QVector< QPair > oldData; for (int row = rect.top(); row <= m_rows.count() && row <= KS_rowMax - rect.height(); ++row) { const int rowStart = m_rows.value(row - 1); const int rowLength = (row < m_rows.count()) ? m_rows.value(row) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); const QVector data = m_data.mid(rowStart, rowLength); // first, iterate over the destination row for (int col = cols.count() - 1; col >= 0; --col) { const int column = cols.value(col); // real column value (1...KS_colMax) if (column >= rect.left() && column <= rect.right()) { // save the old data if (row <= rect.bottom()) oldData.append(qMakePair(QPoint(column, row), data.value(col))); // search const int srcRow = row + rect.height(); const QVector::const_iterator cstart2((srcRow - 1 < m_rows.count()) ? m_cols.begin() + m_rows.value(srcRow - 1) : m_cols.end()); const QVector::const_iterator cend2((srcRow < m_rows.count()) ? (m_cols.begin() + m_rows.value(srcRow)) : m_cols.end()); const QVector::const_iterator cit2 = qBinaryFind(cstart2, cend2, column); // column's missing? if (cit2 == cend2) { m_cols.remove(rowStart + col); m_data.remove(rowStart + col); // adjust the offsets of the following rows for (int r = row; r < m_rows.count(); ++r) --m_rows[r]; } // column exists else { // copy m_data[rowStart + col] = m_data.value(cit2 - m_cols.constBegin()); // remove m_cols.remove(cit2 - m_cols.constBegin()); m_data.remove(cit2 - m_cols.constBegin()); // adjust the offsets of the following rows for (int r = row + rect.height(); r < m_rows.count(); ++r) --m_rows[r]; } } } // last, iterate over the source row const int srcRow = row + rect.height(); const int rowStart2 = (srcRow - 1 < m_rows.count()) ? m_rows.value(srcRow - 1) : m_data.count(); const int rowLength2 = (srcRow < m_rows.count()) ? m_rows.value(srcRow) - rowStart2 : -1; const QVector cols2 = m_cols.mid(rowStart2, rowLength2); const QVector data2 = m_data.mid(rowStart2, rowLength2); int offset = 0; for (int col = cols2.count() - 1; col >= 0; --col) { const int column = cols2.value(col); // real column value (1...KS_colMax) if (column >= rect.left() && column <= rect.right()) { // find the insertion position const QVector::const_iterator cstart((row - 1 < m_rows.count()) ? m_cols.begin() + m_rows.value(row - 1) : m_cols.end()); const QVector::const_iterator cend(((row < m_rows.count())) ? (m_cols.begin() + m_rows.value(row)) : m_cols.end()); const QVector::const_iterator cit = qUpperBound(cstart, cend, cols2.value(col)); // Destination column: const QVector::const_iterator dstcit = qBinaryFind(cols.begin(), cols.end(), column); if (dstcit != cols.end()) { // destination column exists // replace the existing destination value const int dstCol = (dstcit - cols.constBegin()); m_data[rowStart + dstCol] = m_data.value(rowStart2 + col); // remove it from its old position m_data.remove(rowStart2 + col + 1); m_cols.remove(rowStart2 + col + 1); // The amount of values in the range from the // destination row to the source row have not changed. // adjust the offsets of the following rows for (int r = srcRow; r < m_rows.count(); ++r) { ++m_rows[r]; } } else { // destination column does not exist yet // copy it to its new position const int dstCol = cit - m_cols.constBegin(); m_data.insert(dstCol, data2.value(col)); m_cols.insert(dstCol, cols2.value(col)); // remove it from its old position m_data.remove(rowStart2 + col + 1 + offset); m_cols.remove(rowStart2 + col + 1 + offset); ++offset; // adjust the offsets of the following rows for (int r = row; r < srcRow; ++r) { ++m_rows[r]; } } } } } squeezeRows(); return oldData; } /** * Shifts the data in and below \p rect to the bottom by the height of \p rect . * \return the data, that became out of range (shifted over the end) */ QVector< QPair > insertShiftDown(const QRect& rect) { Q_ASSERT(1 <= rect.top() && rect.top() <= KS_rowMax); // row's missing? if (rect.top() > m_rows.count()) return QVector< QPair >(); QVector< QPair > oldData; for (int row = m_rows.count(); row >= rect.top(); --row) { const int rowStart = m_rows.value(row - 1); const int rowLength = (row < m_rows.count()) ? m_rows.value(row) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); const QVector data = m_data.mid(rowStart, rowLength); for (int col = cols.count() - 1; col >= 0; --col) { if (cols.value(col) >= rect.left() && cols.value(col) <= rect.right()) { if (row + rect.height() > KS_rowMax) { // save old data oldData.append(qMakePair(QPoint(cols.value(col), row), data.value(col))); } else { // insert missing rows if (row + rect.height() > m_rows.count()) m_rows.insert(m_rows.count(), row + rect.height() - m_rows.count(), m_data.count()); // copy the data down const int row2 = row + rect.height(); const QVector::const_iterator cstart2(m_cols.begin() + m_rows.value(row2 - 1)); const QVector::const_iterator cend2((row2 < m_rows.count()) ? (m_cols.begin() + m_rows.value(row2)) : m_cols.end()); - const QVector::const_iterator cit2 = qLowerBound(cstart2, cend2, cols.value(col)); + const QVector::const_iterator cit2 = std::lower_bound(cstart2, cend2, cols.value(col)); // column's missing? if (cit2 == cend2 || *cit2 != cols.value(col)) { // determine the index where the data and column has to be inserted const int index = m_rows.value(row2 - 1) + (cit2 - cstart2); // insert the actual data m_data.insert(index, data.value(col)); // insert the column index m_cols.insert(index, cols.value(col)); // adjust the offsets of the following rows for (int r = row2; r < m_rows.count(); ++r) ++m_rows[r]; } // column exists else { const int index = m_rows.value(row2 - 1) + (cit2 - cstart2); m_data[ index ] = data.value(col); } } // remove the data m_cols.remove(rowStart + col); m_data.remove(rowStart + col); // adjust the offsets of the following rows for (int r = row; r < m_rows.count(); ++r) --m_rows[r]; } } } squeezeRows(); return oldData; } /** * Retrieve the first used data in \p col . * Can be used in conjunction with nextInColumn() to loop through a column. * \return the first used data in \p col or the default data, if the column is empty. */ T firstInColumn(int col, int* newRow = 0) const { Q_ASSERT(1 <= col && col <= KS_colMax); const int index = m_cols.indexOf(col); if (newRow) { if (index == -1) // not found *newRow = 0; else *newRow = qUpperBound(m_rows, index) - m_rows.begin(); } return m_data.value(index); } /** * Retrieve the first used data in \p row . * Can be used in conjunction with nextInRow() to loop through a row. * \return the first used data in \p row or the default data, if the row is empty. */ T firstInRow(int row, int* newCol = 0) const { Q_ASSERT(1 <= row && row <= KS_rowMax); // row's empty? if (row > m_rows.count() || ((row < m_rows.count()) && m_rows.value(row - 1) == m_rows.value(row))) { if (newCol) *newCol = 0; return T(); } if (newCol) *newCol = m_cols.value(m_rows.value(row - 1)); return m_data.value(m_rows.value(row - 1)); } /** * Retrieve the last used data in \p col . * Can be used in conjunction with prevInColumn() to loop through a column. * \return the last used data in \p col or the default data, if the column is empty. */ T lastInColumn(int col, int* newRow = 0) const { Q_ASSERT(1 <= col && col <= KS_colMax); const int index = m_cols.lastIndexOf(col); if (newRow) { if (index == -1) // not found *newRow = 0; else *newRow = qUpperBound(m_rows, index) - m_rows.begin(); } return m_data.value(index); } /** * Retrieve the last used data in \p row . * Can be used in conjunction with prevInRow() to loop through a row. * \return the last used data in \p row or the default data, if the row is empty. */ T lastInRow(int row, int* newCol = 0) const { Q_ASSERT(1 <= row && row <= KS_rowMax); // row's empty? if (m_rows.value(row - 1) == m_rows.value(row) || m_rows.value(row - 1) == m_data.count()) { if (newCol) *newCol = 0; return T(); } // last row ends on data vector end if (row == m_rows.count()) { if (newCol) *newCol = m_cols.value(m_data.count() - 1); return m_data.value(m_data.count() - 1); } if (newCol) *newCol = m_cols.value(m_rows.value(row) - 1); return m_data.value(m_rows.value(row) - 1); } /** * Retrieve the next used data in \p col after \p row . * Can be used in conjunction with firstInColumn() to loop through a column. * \return the next used data in \p col or the default data, there is no further data. */ T nextInColumn(int col, int row, int* newRow = 0) const { Q_ASSERT(1 <= col && col <= KS_colMax); Q_ASSERT(1 <= row && row <= KS_rowMax); // no next row? if (row + 1 > m_rows.count()) { if (newRow) *newRow = 0; return T(); } // search beginning in rows after the specified row const int index = m_cols.indexOf(col, m_rows.value(row)); if (newRow) { if (index == -1) // not found *newRow = 0; else *newRow = qUpperBound(m_rows, index) - m_rows.begin(); } return m_data.value(index); } /** * Retrieve the next used data in \p row after \p col . * Can be used in conjunction with firstInRow() to loop through a row. * \return the next used data in \p row or the default data, if there is no further data. */ T nextInRow(int col, int row, int* newCol = 0) const { Q_ASSERT(1 <= col && col <= KS_colMax); Q_ASSERT(1 <= row && row <= KS_rowMax); // is the row not present? if (row > m_rows.count()) { if (newCol) *newCol = 0; return T(); } const QVector::const_iterator cstart(m_cols.begin() + m_rows.value(row - 1)); const QVector::const_iterator cend((row < m_rows.count()) ? (m_cols.begin() + m_rows.value(row)) : m_cols.end()); const QVector::const_iterator cit = qUpperBound(cstart, cend, col); if (cit == cend || *cit <= col) { if (newCol) *newCol = 0; return T(); } if (newCol) *newCol = m_cols.value(m_rows.value(row - 1) + (cit - cstart)); return m_data.value(m_rows.value(row - 1) + (cit - cstart)); } /** * Retrieve the previous used data in \p col after \p row . * Can be used in conjunction with lastInColumn() to loop through a column. * \return the previous used data in \p col or the default data, there is no further data. */ T prevInColumn(int col, int row, int* newRow = 0) const { Q_ASSERT(1 <= col && col <= KS_colMax); Q_ASSERT(1 <= row && row <= KS_rowMax); // first row? if (row <= m_rows.count() && m_rows.value(row - 1) == 0) { if (newRow) *newRow = 0; return T(); } const int index = m_cols.lastIndexOf(col, m_rows.value(row - 1) - 1); if (newRow) { if (index == -1) // not found *newRow = 0; else *newRow = qUpperBound(m_rows, index) - m_rows.begin(); } return m_data.value(index); } /** * Retrieve the previous used data in \p row after \p col . * Can be used in conjunction with lastInRow() to loop through a row. * \return the previous used data in \p row or the default data, if there is no further data. */ T prevInRow(int col, int row, int* newCol = 0) const { Q_ASSERT(1 <= col && col <= KS_colMax); Q_ASSERT(1 <= row && row <= KS_rowMax); const QVector::const_iterator cstart((row - 1 < m_rows.count()) ? m_cols.begin() + m_rows.value(row - 1) : m_cols.end()); const QVector::const_iterator cend((row < m_rows.count()) ? (m_cols.begin() + m_rows.value(row)) : m_cols.end()); - const QVector::const_iterator cit = qLowerBound(cstart, cend, col); + const QVector::const_iterator cit = std::lower_bound(cstart, cend, col); if (cit == cstart) { if (newCol) *newCol = 0; return T(); } if (newCol) *newCol = m_cols.value(cit - 1 - m_cols.begin()); return m_data.value(cit - 1 - m_cols.begin()); } /** * For debugging/testing purposes. * \note only works with primitive/printable data */ QString dump() const { QString str; // determine the dimension of the matrix (the missing column number) int maxCols = 0; for (int row = 0; row < m_rows.count(); ++row) { const int rowStart = m_rows.value(row); const int rowLength = (row + 1 < m_rows.count()) ? m_rows.value(row + 1) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); maxCols = qMax(maxCols, cols.value(cols.count() - 1)); } for (int row = 0; row < m_rows.count(); ++row) { str += '('; const int rowStart = m_rows.value(row); const int rowLength = (row + 1 < m_rows.count()) ? m_rows.value(row + 1) - rowStart : -1; const QVector cols = m_cols.mid(rowStart, rowLength); const QVector data = m_data.mid(rowStart, rowLength); int lastCol = 0; for (int col = 0; col < cols.count(); ++col) { int counter = cols.value(col) - lastCol; while (counter-- > 1) str += " ,"; str += QString("%1,").arg(data.value(col), 2); // str += QString( "%1," ).arg( (data.value( col ) == T()) ? "" : "_", 2 ); lastCol = cols.value(col); } // fill the column up to the max int counter = maxCols - lastCol; while (counter-- > 0) str += " ,"; // replace the last comma str[str.length()-1] = ')'; str += '\n'; } return str.isEmpty() ? QString("()") : str.mid(0, str.length() - 1); } /** * Returns the column of the non-default data at \p index . * \return the data's column at \p index . * \see count() * \see row() * \see data() */ int col(int index) const { return m_cols.value(index); } /** * Returns the row of the non-default data at \p index . * \return the data's row at \p index . * \see count() * \see col() * \see data() */ int row(int index) const { return qUpperBound(m_rows, index) - m_rows.begin(); } /** * Returns the non-default data at \p index . * \return the data at \p index . * \see count() * \see col() * \see row() */ T data(int index) const { return m_data.value(index); } /** * The maximum occupied column, i.e. the horizontal storage dimension. * \return the maximum column */ int columns() const { int columns = 0; for (int c = 0; c < m_cols.count(); ++c) columns = qMax(m_cols.value(c), columns); return columns; } /** * The maximum occupied row, i.e. the vertical storage dimension. * \return the maximum row */ int rows() const { return m_rows.count(); } /** * Creates a substorage consisting of the values in \p region. * If \p keepOffset is \c true, the values' positions are not altered. * Otherwise, the upper left of \p region's bounding rect is used as new origin, * and all positions are adjusted. * \return a subset of the storage stripped down to the values in \p region */ PointStorage subStorage(const Region& region, bool keepOffset = true) const { // Determine the offset. const QPoint offset = keepOffset ? QPoint(0, 0) : region.boundingRect().topLeft() - QPoint(1, 1); // this generates an array of values PointStorage subStorage; Region::ConstIterator end(region.constEnd()); for (Region::ConstIterator it(region.constBegin()); it != end; ++it) { const QRect rect = (*it)->rect(); for (int row = rect.top(); row <= rect.bottom() && row <= m_rows.count(); ++row) { const QVector::const_iterator cstart(m_cols.begin() + m_rows.value(row - 1)); const QVector::const_iterator cend((row < m_rows.count()) ? (m_cols.begin() + m_rows.value(row)) : m_cols.end()); for (QVector::const_iterator cit = cstart; cit != cend; ++cit) { if (*cit >= rect.left() && *cit <= rect.right()) { if (keepOffset) subStorage.insert(*cit, row, m_data.value(cit - m_cols.begin())); else subStorage.insert(*cit - offset.x(), row - offset.y(), m_data.value(cit - m_cols.begin())); } } } } return subStorage; } /** * Equality operator. */ bool operator==(const PointStorage& o) const { return (m_rows == o.m_rows && m_cols == o.m_cols && m_data == o.m_data); } private: void squeezeRows() { int row = m_rows.count() - 1; while (m_rows.value(row) == m_data.count() && row >= 0) m_rows.remove(row--); } private: QVector m_cols; // stores the column indices (beginning with one) QVector m_rows; // stores the row offsets in m_data QVector m_data; // stores the actual non-default data #ifdef KSPREAD_POINT_STORAGE_HASH QSet m_usedData; #endif }; } // namespace Sheets } // namespace Calligra #endif // CALLIGRA_SHEETS_POINT_STORAGE diff --git a/sheets/dialogs/SortDialog.cpp b/sheets/dialogs/SortDialog.cpp index b0572d96876..527f8d77e42 100644 --- a/sheets/dialogs/SortDialog.cpp +++ b/sheets/dialogs/SortDialog.cpp @@ -1,698 +1,698 @@ /* This file is part of the KDE project Copyright (C) 2006 Robert Knight (C) 2006 Tomas Mecir (C) 2002-2003 Norbert Andres (C) 2002 Ariya Hidayat (C) 2002 John Dailey (C) 2002 Werner Trobin (C) 2001-2002 Philipp Mueller (C) 1999-2002 Laurent Montel (C) 2000 David Faure (C) 1998-2000 Torben Weis This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Local #include "SortDialog.h" // Sheets #include "Map.h" #include "ui/Selection.h" #include "Sheet.h" #include "ValueConverter.h" // commands #include "commands/SortManipulator.h" #include // ui #include "ui_SortWidget.h" #include "ui_SortDetailsWidget.h" #include // Qt #include #include using namespace Calligra::Sheets; Q_DECLARE_METATYPE(Qt::CaseSensitivity) Q_DECLARE_METATYPE(Qt::SortOrder) class SortDialog::Private : public QStyledItemDelegate { public: Private(SortDialog *parent = 0) : QStyledItemDelegate(parent) { } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override { Q_UNUSED(index) Q_UNUSED(option) if (mainWidget.m_sortHorizontal->isChecked()) /* data grouped in columns; criteria/header per row */ { if (rows.isEmpty()) { return 0; } } else if (columns.isEmpty()) { return 0; } KComboBox *const combo = new KComboBox(parent); return combo; } void setEditorData(QWidget *editor, const QModelIndex &index) const override { if (!index.isValid()) { return; } KComboBox *const combo = static_cast(editor); const QAbstractItemModel *const model = index.model(); const QString itemText = model->data(index, Qt::DisplayRole).toString(); const int itemIndex = model->data(index, Qt::UserRole).toInt(); const bool hasHeader = mainWidget.m_useHeader->isChecked(); Sheet *const sheet = selection->lastSheet(); ValueConverter *const converter = sheet->map()->converter(); if (mainWidget.m_sortVertical->isChecked()) /* data grouped in rows; criteria/header per column */ { // Put the old item back into the map of available items. insertIndex(itemIndex, Qt::Horizontal); const int row = selection->lastRange().top(); const QList indices = columns; for (int i = 0; i < indices.count(); ++i) { const int col = indices[i]; const QString columnName = i18n("Column %1", Cell::columnName(col)); const Value value = Cell(sheet, col, row).value(); const QString header = converter->asString(value).asString(); if (hasHeader) { if (header.isEmpty()) { combo->addItem('(' + columnName + ')', col); } else { combo->addItem(header, col); combo->setItemData(combo->count() - 1, columnName, Qt::ToolTipRole); } } else { combo->addItem(columnName, col); } if (col == itemIndex) { combo->setCurrentIndex(i); } } } else /* row headers */ { // Put the old item back into the map of available items. insertIndex(itemIndex, Qt::Vertical); const int col = selection->lastRange().left(); const QList indices = rows; for (int i = 0; i < indices.count(); ++i) { const int row = indices[i]; const QString rowName = i18n("Row %1", row); const Value value = Cell(sheet, col, row).value(); const QString header = converter->asString(value).asString(); if (hasHeader) { if (header.isEmpty()) { combo->addItem('(' + rowName + ')', row); } else { combo->addItem(header, row); combo->setItemData(combo->count() - 1, rowName, Qt::ToolTipRole); } } else { combo->addItem(rowName, row); } if (row == itemIndex) { combo->setCurrentIndex(i); } } } } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { KComboBox *const combo = static_cast(editor); const int currentIndex = combo->currentIndex(); model->setData(index, combo->itemText(currentIndex), Qt::DisplayRole); model->setData(index, combo->itemData(currentIndex), Qt::UserRole); // Remove the current item from the map of available items. if (mainWidget.m_sortHorizontal->isChecked()) /* data grouped in columns; criteria/header per row */ { rows.removeAll(combo->itemData(currentIndex).toInt()); } else { columns.removeAll(combo->itemData(currentIndex).toInt()); } } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override { Q_UNUSED(index) editor->setGeometry(option.rect); } public: // data Selection *selection; Ui::SortWidget mainWidget; Ui::SortDetailsWidget detailsWidget; mutable QList columns; mutable QList rows; public: /// \return \c true if all columns/rows have text values bool hasHeader(const Region ®ion, Qt::Orientation orientation) const; void createAvailableIndices(const Region ®ion, Qt::Orientation orientation); void insertIndex(int index, Qt::Orientation orientation) const; QString itemText(int index, bool useHeader) const; void initCriteria(Qt::Orientation orientation, SortDialog *parent); }; bool SortDialog::Private::hasHeader(const Region ®ion, Qt::Orientation orientation) const { Sheet *const sheet = region.lastSheet(); const QRect range = region.lastRange(); if (orientation == Qt::Horizontal) /* check for column headers */ { for (int col = range.left(); col <= range.right(); ++col) { if (!Cell(sheet, col, range.top()).value().isString()) { return false; } } } else /* check for row headers */ { for (int row = range.top(); row <= range.bottom(); ++row) { if (!Cell(sheet, range.left(), row).value().isString()) { return false; } } } return true; } void SortDialog::Private::createAvailableIndices(const Region ®ion, Qt::Orientation orientation) { const QRect range = region.lastRange(); if (orientation == Qt::Horizontal) /* available columns */ { for (int col = range.left(); col <= range.right(); ++col) { columns.append(col); } } else /* available rows */ { for (int row = range.top(); row <= range.bottom(); ++row) { rows.append(row); } } } void SortDialog::Private::insertIndex(int index, Qt::Orientation orientation) const { if (orientation == Qt::Vertical) /* data grouped in columns; criteria/header per row */ { Q_ASSERT(1 <= index && index <= KS_colMax); - QList::Iterator it = qLowerBound(rows.begin(), rows.end(), index); + QList::Iterator it = std::lower_bound(rows.begin(), rows.end(), index); if (*it == index) { return; } rows.insert(it, index); } else /* data grouped in rows; criteria/header per column */ { Q_ASSERT(1 <= index && index <= KS_rowMax); - QList::Iterator it = qLowerBound(columns.begin(), columns.end(), index); + QList::Iterator it = std::lower_bound(columns.begin(), columns.end(), index); if (*it == index) { return; } columns.insert(it, index); } } QString SortDialog::Private::itemText(int index, bool useHeader) const { Sheet *const sheet = selection->lastSheet(); ValueConverter *const converter = sheet->map()->converter(); if (mainWidget.m_sortHorizontal->isChecked()) /* data grouped in columns; criteria/header per row */ { const int col = selection->lastRange().left(); const int row = index; const QString rowName = i18n("Row %1", row); if (useHeader) { const Value value = Cell(sheet, col, row).value(); const QString header = converter->asString(value).asString(); if (header.isEmpty()) { return QString('(' + rowName + ')'); } else { return header; } } else { return rowName; } } else /* data grouped in rows; criteria/header per column */ { const int col = index; const int row = selection->lastRange().top(); const QString columnName = i18n("Column %1", Cell::columnName(col)); if (useHeader) { const Value value = Cell(sheet, col, row).value(); const QString header = converter->asString(value).asString(); if (header.isEmpty()) { return QString('(' + columnName + ')'); } else { return header; } } else { return columnName; } } } void SortDialog::Private::initCriteria(Qt::Orientation orientation, SortDialog *parent) { // Put the items back into the map of available items. for (int row = mainWidget.m_tableWidget->rowCount() - 1; row >= 0; --row) { QTableWidgetItem *const item = mainWidget.m_tableWidget->item(row, 0); const int index = item->data(Qt::UserRole).toInt(); insertIndex(index, orientation); mainWidget.m_tableWidget->removeRow(row); } // (Re-)Insert the criteria. if (mainWidget.m_sortHorizontal->isChecked()) { while (rows.count()) { parent->addCriterion(); } } else { while (columns.count()) { parent->addCriterion(); } } // Setup the buttons. mainWidget.m_addButton->setEnabled(false); mainWidget.m_removeButton->setEnabled(false); mainWidget.m_upButton->setEnabled(false); mainWidget.m_downButton->setEnabled(false); // Adjust the header usage text. if (mainWidget.m_sortHorizontal->isChecked()) /* Sort horizontally */ { // data gets sorted horizontally; comparisons per row; columns get exchanged/sorted mainWidget.m_useHeader->setText(i18n("&First column contains row headers")); } else /* Sort vertically */ { // data gets sorted vertically; comparisons per column; rows get exchanged/sorted mainWidget.m_useHeader->setText(i18n("&First row contains column headers")); } } SortDialog::SortDialog(QWidget* parent, Selection* selection) : KoDialog(parent) , d(new Private(this)) { d->selection = selection; setCaption(i18n("Sort")); setButtons(Ok | Cancel | Details | Reset); setObjectName(QLatin1String("SortDialog")); QWidget *widget = new QWidget(this); d->mainWidget.setupUi(widget); setMainWidget(widget); widget = new QWidget(this); d->detailsWidget.setupUi(widget); setDetailsWidget(widget); // UI refinements Designer is not capable of d->mainWidget.m_addButton->setIcon(koIcon("list-add")); d->mainWidget.m_removeButton->setIcon(koIcon("list-remove")); d->mainWidget.m_upButton->setIcon(koIcon("go-up")); d->mainWidget.m_downButton->setIcon(koIcon("go-down")); QHeaderView *const header = d->mainWidget.m_tableWidget->horizontalHeader(); header->setSectionResizeMode(QHeaderView::ResizeToContents); header->setSectionResizeMode(0, QHeaderView::Stretch); d->mainWidget.m_tableWidget->setItemDelegateForColumn(0, d); connect(d->mainWidget.m_useHeader, SIGNAL(toggled(bool)), this, SLOT(useHeaderChanged(bool))); connect(d->mainWidget.m_sortHorizontal, SIGNAL(toggled(bool)), this, SLOT(orientationChanged(bool))); connect(d->mainWidget.m_tableWidget, SIGNAL(itemActivated(QTableWidgetItem*)), this, SLOT(itemActivated(QTableWidgetItem*))); connect(d->mainWidget.m_tableWidget, SIGNAL(itemSelectionChanged()), this, SLOT(itemSelectionChanged())); connect(d->mainWidget.m_addButton, SIGNAL(clicked()), this, SLOT(addCriterion())); connect(d->mainWidget.m_removeButton, SIGNAL(clicked()), this, SLOT(removeCriterion())); connect(d->mainWidget.m_upButton, SIGNAL(clicked()), this, SLOT(moveCriterionUp())); connect(d->mainWidget.m_downButton, SIGNAL(clicked()), this, SLOT(moveCriterionDown())); init(); } SortDialog::~SortDialog() { delete d; } void SortDialog::init() { QStringList lst; lst << i18n("January") + ',' + i18n("February") + ',' + i18n("March") + ',' + i18n("April") + ',' + i18n("May") + ',' + i18n("June") + ',' + i18n("July") + ',' + i18n("August") + ',' + i18n("September") + ',' + i18n("October") + ',' + i18n("November") + ',' + i18n("December"); lst << i18n("Monday") + ',' + i18n("Tuesday") + ',' + i18n("Wednesday") + ',' + i18n("Thursday") + ',' + i18n("Friday") + ',' + i18n("Saturday") + ',' + i18n("Sunday"); KSharedConfigPtr config = KSharedConfig::openConfig(); const QStringList other = config->group("Parameters").readEntry("Other list", QStringList()); QString tmp; for (QStringList::ConstIterator it = other.begin(); it != other.end(); ++it) { if ((*it) != "\\") tmp += (*it) + ", "; else if (it != other.begin()) { tmp = tmp.left(tmp.length() - 2); lst.append(tmp); tmp.clear(); } } d->detailsWidget.m_customList->insertItems(0, lst); Sheet *const sheet = d->selection->lastSheet(); const QRect range = d->selection->lastRange(); const Region region(range, sheet); if (region.isColumnSelected()) /* entire columns */ { d->mainWidget.m_sortHorizontal->setEnabled(false); d->mainWidget.m_sortVertical->setChecked(true); const bool hasHeader = d->hasHeader(region, Qt::Horizontal); d->mainWidget.m_useHeader->setChecked(hasHeader); d->createAvailableIndices(region, Qt::Horizontal); } else if (region.isRowSelected()) /* entire rows */ { d->mainWidget.m_sortVertical->setEnabled(false); d->mainWidget.m_sortHorizontal->setChecked(true); const bool hasHeader = d->hasHeader(region, Qt::Vertical); d->mainWidget.m_useHeader->setChecked(hasHeader); d->createAvailableIndices(region, Qt::Vertical); } else /* ordinary cell range */ { if (range.top() == range.bottom()) /* only one row */{ d->mainWidget.m_sortVertical->setEnabled(false); d->mainWidget.m_sortHorizontal->setChecked(true); } else if (range.left() == range.right()) /* only one column */ { d->mainWidget.m_sortHorizontal->setEnabled(false); d->mainWidget.m_sortVertical->setChecked(true); } else { const bool hasColumnHeader = d->hasHeader(region, Qt::Horizontal); const bool hasRowHeader = d->hasHeader(region, Qt::Vertical); #if 0 // TODO if (hasColumnHeader && range.top() + 1 == range.bottom()) /* only one data row */ { d->mainWidget.m_sortVertical->setEnabled(false); } if (hasRowHeader && range.left() + 1 == range.right()) /* only one data column */ { d->mainWidget.m_sortHorizontal->setEnabled(false); } #endif if (range.width() >= range.height()) { d->mainWidget.m_sortHorizontal->setChecked(true); d->mainWidget.m_useHeader->setChecked(hasRowHeader); } else { d->mainWidget.m_sortVertical->setChecked(true); d->mainWidget.m_useHeader->setChecked(hasColumnHeader); } } // create column indices, if data can be sorted vertically if (d->mainWidget.m_sortVertical->isEnabled()) { d->createAvailableIndices(region, Qt::Horizontal); } // create row indices, if data can be sorted horizontally if (d->mainWidget.m_sortHorizontal->isEnabled()) { d->createAvailableIndices(region, Qt::Vertical); } } // Initialize the criteria. slotButtonClicked(Reset); } void SortDialog::orientationChanged(bool horizontal) { // Take the old, i.e. the reverse orientation. const Qt::Orientation orientation = horizontal ? Qt::Horizontal : Qt::Vertical; d->initCriteria(orientation, this); } void SortDialog::accept() { Sheet *const sheet = d->selection->activeSheet(); SortManipulator *const command = new SortManipulator(); command->setSheet(sheet); // set parameters command->setSortRows(d->mainWidget.m_sortVertical->isChecked()); command->setSkipFirst(d->mainWidget.m_useHeader->isChecked()); command->setCopyFormat(d->detailsWidget.m_copyLayout->isChecked()); const bool horizontal = d->mainWidget.m_sortHorizontal->isChecked(); const QRect range = d->selection->lastRange(); const int offset = horizontal ? range.top() : range.left(); // retrieve sorting order QTableWidget *const table = d->mainWidget.m_tableWidget; for (int i = 0; i < table->rowCount(); ++i) { const int index = table->item(i, 0)->data(Qt::UserRole).toInt(); const Qt::SortOrder order = table->item(i, 1)->data(Qt::UserRole).value(); const Qt::CaseSensitivity caseSensitivity = table->item(i, 2)->data(Qt::UserRole).value(); command->addCriterion(index - offset, order, caseSensitivity); } if (d->detailsWidget.m_useCustomLists->isChecked()) { // add custom list if any QStringList clist; QString list = d->detailsWidget.m_customList->currentText(); QString tmp; int l = list.length(); for (int i = 0; i < l; ++i) { if (list[i] == ',') { clist.append(tmp.trimmed()); tmp.clear(); } else tmp += list[i]; } command->setUseCustomList(true); command->setCustomList(clist); } command->add(d->selection->lastRange()); command->execute(d->selection->canvas()); d->selection->emitModified(); KoDialog::accept(); } void SortDialog::slotButtonClicked(int button) { if (button == Reset) { const bool horizontal = d->mainWidget.m_sortHorizontal->isChecked(); const Qt::Orientation orientation = horizontal ? Qt::Vertical : Qt::Horizontal; d->initCriteria(orientation, this); } KoDialog::slotButtonClicked(button); } void SortDialog::useHeaderChanged(bool enable) { // Rename the list items. QTableWidget *const table = d->mainWidget.m_tableWidget; for (int row = 0; row < table->rowCount(); ++row) { QTableWidgetItem *const item = table->item(row, 0); const int index = item->data(Qt::UserRole).toInt(); item->setText(d->itemText(index, enable)); } } void SortDialog::itemActivated(QTableWidgetItem *item) { if (item->column() == 1) /* Sort Order */ { if (item->data(Qt::UserRole).value() == Qt::AscendingOrder) { item->setIcon(koIcon("view-sort-descending")); item->setText(i18n("Descending")); item->setData(Qt::UserRole, QVariant::fromValue(Qt::DescendingOrder)); } else { item->setIcon(koIcon("view-sort-ascending")); item->setText(i18n("Ascending")); item->setData(Qt::UserRole, QVariant::fromValue(Qt::AscendingOrder)); } } else if (item->column() == 2) /* Case Sensitivity */ { if (item->checkState() == Qt::Checked) { item->setCheckState(Qt::Unchecked); item->setText(i18n("Case Insensitive")); item->setData(Qt::UserRole, QVariant::fromValue(Qt::CaseInsensitive)); } else { item->setCheckState(Qt::Checked); item->setText(i18n("Case Sensitive")); item->setData(Qt::UserRole, QVariant::fromValue(Qt::CaseSensitive)); } } } void SortDialog::itemSelectionChanged() { QTableWidget *const table = d->mainWidget.m_tableWidget; QList ranges = table->selectedRanges(); if (ranges.count() == 0) { d->mainWidget.m_removeButton->setEnabled(false); d->mainWidget.m_upButton->setEnabled(false); d->mainWidget.m_downButton->setEnabled(false); } else { d->mainWidget.m_removeButton->setEnabled(true); bool first = false; bool last = false; for (int i = 0; i < ranges.count(); ++i) { if (ranges[i].topRow() == 0) { first = true; } if (ranges[i].bottomRow() == table->rowCount() - 1) { last = true; } if (first && last) { break; } } d->mainWidget.m_upButton->setEnabled(!first); d->mainWidget.m_downButton->setEnabled(!last); } } void SortDialog::addCriterion() { QTableWidgetItem *item; const bool useHeader = d->mainWidget.m_useHeader->isChecked(); // Take the first item from the map of available items. if (d->mainWidget.m_sortVertical->isChecked()) /* data grouped in rows; criteria/header per column */ { const QList keys = d->columns; if (keys.isEmpty()) { return; } else if (keys.count() == 1) { d->mainWidget.m_addButton->setEnabled(false); } const int col = d->columns.takeFirst(); item = new QTableWidgetItem(d->itemText(col, useHeader)); item->setData(Qt::UserRole, col); } else { const QList keys = d->rows; if (keys.isEmpty()) { return; } else if (keys.count() == 1) { d->mainWidget.m_addButton->setEnabled(false); } const int row = d->rows.takeFirst(); item = new QTableWidgetItem(d->itemText(row, useHeader)); item->setData(Qt::UserRole, row); } // Insert the item and its default attributes in a new row. const int row = d->mainWidget.m_tableWidget->rowCount(); d->mainWidget.m_tableWidget->insertRow(row); item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable); d->mainWidget.m_tableWidget->setItem(row, 0, item); item = new QTableWidgetItem(koIcon("view-sort-ascending"), i18n("Ascending")); item->setData(Qt::UserRole, Qt::AscendingOrder); item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable); d->mainWidget.m_tableWidget->setItem(row, 1, item); item = new QTableWidgetItem(i18n("Case Sensitive")); item->setCheckState(Qt::Checked); item->setData(Qt::UserRole, Qt::CaseSensitive); item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable); d->mainWidget.m_tableWidget->setItem(row, 2, item); } bool greaterThan(const QTableWidgetSelectionRange &r1, const QTableWidgetSelectionRange &r2) { return r1.topRow() > r2.topRow(); } void SortDialog::removeCriterion() { QTableWidget *const table = d->mainWidget.m_tableWidget; QList ranges = table->selectedRanges(); if (ranges.isEmpty()) { return; } std::stable_sort(ranges.begin(), ranges.end(), greaterThan); for (int i = 0; i < ranges.count(); ++i) { for (int row = ranges[i].bottomRow(); row >= ranges[i].topRow(); --row) { // Reinsert the item to be removed into the map of available items. const int index = table->item(row, 0)->data(Qt::UserRole).toInt(); if (d->mainWidget.m_sortHorizontal->isChecked()) { d->insertIndex(index, Qt::Vertical); } else { d->insertIndex(index, Qt::Horizontal); } // Remove the item from the list. table->removeRow(row); } } d->mainWidget.m_addButton->setEnabled(true); } void SortDialog::moveCriterionUp() { QTableWidget *const table = d->mainWidget.m_tableWidget; const QList ranges = table->selectedRanges(); for (int i = 0; i < ranges.count(); ++i) { if (ranges[i].topRow() > 0) { const int srcRow = ranges[i].topRow() - 1; const int dstRow = ranges[i].bottomRow() + 1; table->insertRow(dstRow); for (int col = 0; col <= 2; ++col) { table->setItem(dstRow, col, table->takeItem(srcRow, col)); } table->removeRow(srcRow); } } itemSelectionChanged(); } void SortDialog::moveCriterionDown() { QTableWidget *const table = d->mainWidget.m_tableWidget; const QList ranges = table->selectedRanges(); for (int i = 0; i < ranges.count(); ++i) { if (ranges[i].bottomRow() < table->rowCount() - 1) { const int srcRow = ranges[i].bottomRow() + 2; const int dstRow = ranges[i].topRow(); table->insertRow(dstRow); for (int col = 0; col <= 2; ++col) { table->setItem(dstRow, col, table->takeItem(srcRow, col)); } table->removeRow(srcRow); } } itemSelectionChanged(); }