/*************************************************************************** * Copyright (c) Eivind Kvedalen (eivind@kvedalen.name) 2015 * * * * This file is part of the FreeCAD CAx development system. * * * * 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., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include "PreCompiled.h" #ifndef _PreComp_ # include # include # include # include # include # include # include # include # include #endif # include #include #include #include #include #include #include #include #include #include #include #include "../App/Utils.h" #include "../App/Cell.h" #include #include "SheetTableView.h" #include "LineEdit.h" #include "PropertiesDialog.h" #include "DlgBindSheet.h" #include "DlgSheetConf.h" using namespace SpreadsheetGui; using namespace Spreadsheet; using namespace App; namespace bp = boost::placeholders; void SheetViewHeader::mouseReleaseEvent(QMouseEvent *event) { QHeaderView::mouseReleaseEvent(event); resizeFinished(); } bool SheetViewHeader::viewportEvent(QEvent *e) { if(e->type() == QEvent::ContextMenu) { auto *ce = static_cast(e); int section = logicalIndexAt(ce->pos()); if(section>=0) { if(orientation() == Qt::Horizontal) { if(!owner->selectionModel()->isColumnSelected(section,owner->rootIndex())) { owner->clearSelection(); owner->selectColumn(section); } }else if(!owner->selectionModel()->isRowSelected(section,owner->rootIndex())) { owner->clearSelection(); owner->selectRow(section); } } } return QHeaderView::viewportEvent(e); } static std::pair selectedMinMaxRows(QModelIndexList list) { int min = std::numeric_limits::max(); int max = 0; for (const auto & item : list) { int row = item.row(); min = std::min(row, min); max = std::max(row, max); } return {min, max}; } static std::pair selectedMinMaxColumns(QModelIndexList list) { int min = std::numeric_limits::max(); int max = 0; for (const auto & item : list) { int column = item.column(); min = std::min(column, min); max = std::max(column, max); } return {min, max}; } SheetTableView::SheetTableView(QWidget *parent) : QTableView(parent) , sheet(0) , tabCounter(0) { setHorizontalHeader(new SheetViewHeader(this,Qt::Horizontal)); setVerticalHeader(new SheetViewHeader(this,Qt::Vertical)); setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); connect(verticalHeader(), &QWidget::customContextMenuRequested, [this](const QPoint &point){ QMenu menu(this); const auto selection = selectionModel()->selectedRows(); const auto & [min, max] = selectedMinMaxRows(selection); if (bool isContiguous = max - min == selection.size() - 1) { Q_UNUSED(isContiguous) /*: This is shown in the context menu for the vertical header in a spreadsheet. The number refers to how many lines are selected and will be inserted. */ auto insertBefore = menu.addAction(tr("Insert %n row(s) above", "", selection.size())); connect(insertBefore, SIGNAL(triggered()), this, SLOT(insertRows())); if (max < model()->rowCount() - 1) { auto insertAfter = menu.addAction(tr("Insert %n row(s) below", "", selection.size())); connect(insertAfter, SIGNAL(triggered()), this, SLOT(insertRowsAfter())); } } else { auto insert = menu.addAction(tr("Insert %n non-contiguous rows", "", selection.size())); connect(insert, SIGNAL(triggered()), this, SLOT(insertRows())); } auto remove = menu.addAction(tr("Remove row(s)", "", selection.size())); connect(remove, SIGNAL(triggered()), this, SLOT(removeRows())); menu.exec(verticalHeader()->mapToGlobal(point)); }); connect(horizontalHeader(), &QWidget::customContextMenuRequested, [this](const QPoint &point){ QMenu menu(this); const auto selection = selectionModel()->selectedColumns(); const auto & [min, max] = selectedMinMaxColumns(selection); if (bool isContiguous = max - min == selection.size() - 1) { Q_UNUSED(isContiguous) /*: This is shown in the context menu for the horizontal header in a spreadsheet. The number refers to how many lines are selected and will be inserted. */ auto insertAbove = menu.addAction(tr("Insert %n column(s) left", "", selection.size())); connect(insertAbove, SIGNAL(triggered()), this, SLOT(insertColumns())); if (max < model()->columnCount() - 1) { auto insertAfter = menu.addAction(tr("Insert %n column(s) right", "", selection.size())); connect(insertAfter, SIGNAL(triggered()), this, SLOT(insertColumnsAfter())); } } else { auto insert = menu.addAction(tr("Insert %n non-contiguous columns", "", selection.size())); connect(insert, SIGNAL(triggered()), this, SLOT(insertColumns())); } auto remove = menu.addAction(tr("Remove column(s)", "", selection.size())); connect(remove, SIGNAL(triggered()), this, SLOT(removeColumns())); menu.exec(horizontalHeader()->mapToGlobal(point)); }); auto cellProperties = new QAction(tr("Properties..."), this); addAction(cellProperties); horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); verticalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); contextMenu = new QMenu(this); contextMenu->addAction(cellProperties); connect(cellProperties, SIGNAL(triggered()), this, SLOT(cellProperties())); contextMenu->addSeparator(); QAction *recompute = new QAction(tr("Recompute"),this); connect(recompute, SIGNAL(triggered()), this, SLOT(onRecompute())); contextMenu->addAction(recompute); actionBind = new QAction(tr("Bind..."),this); connect(actionBind, SIGNAL(triggered()), this, SLOT(onBind())); contextMenu->addAction(actionBind); QAction *actionConf = new QAction(tr("Configuration table..."),this); connect(actionConf, SIGNAL(triggered()), this, SLOT(onConfSetup())); contextMenu->addAction(actionConf); horizontalHeader()->addAction(actionBind); verticalHeader()->addAction(actionBind); contextMenu->addSeparator(); actionMerge = contextMenu->addAction(tr("Merge cells")); connect(actionMerge,SIGNAL(triggered()), this, SLOT(mergeCells())); actionSplit = contextMenu->addAction(tr("Split cells")); connect(actionSplit,SIGNAL(triggered()), this, SLOT(splitCell())); contextMenu->addSeparator(); actionCut = contextMenu->addAction(tr("Cut")); connect(actionCut,SIGNAL(triggered()), this, SLOT(cutSelection())); actionCopy = contextMenu->addAction(tr("Copy")); connect(actionCopy,SIGNAL(triggered()), this, SLOT(copySelection())); actionPaste = contextMenu->addAction(tr("Paste")); connect(actionPaste,SIGNAL(triggered()), this, SLOT(pasteClipboard())); actionDel = contextMenu->addAction(tr("Delete")); connect(actionDel,SIGNAL(triggered()), this, SLOT(deleteSelection())); setTabKeyNavigation(false); } void SheetTableView::onRecompute() { Gui::Command::openCommand("Recompute cells"); for(auto &range : selectedRanges()) { Gui::cmdAppObjectArgs(sheet, "recomputeCells('%s', '%s')", range.fromCellString(), range.toCellString()); } Gui::Command::commitCommand(); } void SheetTableView::onBind() { auto ranges = selectedRanges(); if(ranges.size()>=1 && ranges.size()<=2) { DlgBindSheet dlg(sheet,ranges,this); dlg.exec(); } } void SheetTableView::onConfSetup() { auto ranges = selectedRanges(); if(ranges.empty()) return; DlgSheetConf dlg(sheet,ranges.back(),this); dlg.exec(); } void SheetTableView::cellProperties() { std::unique_ptr dialog(new PropertiesDialog(sheet, selectedRanges(), this)); if (dialog->exec() == QDialog::Accepted) { dialog->apply(); } } std::vector SheetTableView::selectedRanges() const { QModelIndexList list = selectionModel()->selectedIndexes(); std::vector result; // Insert selected cells into set. This variable should ideally be a hash_set // but that is not part of standard stl. std::set > cells; for (QModelIndexList::const_iterator it = list.begin(); it != list.end(); ++it) cells.insert(std::make_pair((*it).row(), (*it).column())); // Create rectangular cells from the unordered collection of selected cells std::map, std::pair > rectangles; createRectangles(cells, rectangles); std::map, std::pair >::const_iterator i = rectangles.begin(); for (; i != rectangles.end(); ++i) { std::pair ul = (*i).first; std::pair size = (*i).second; result.emplace_back(ul.first, ul.second, ul.first + size.first - 1, ul.second + size.second - 1); } return result; } void SheetTableView::insertRows() { assert(sheet != 0); QModelIndexList rows = selectionModel()->selectedRows(); std::vector sortedRows; /* Make sure rows are sorted in ascending order */ for (QModelIndexList::const_iterator it = rows.begin(); it != rows.end(); ++it) sortedRows.push_back(it->row()); std::sort(sortedRows.begin(), sortedRows.end()); /* Insert rows */ Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Insert rows")); std::vector::const_reverse_iterator it = sortedRows.rbegin(); while (it != sortedRows.rend()) { int prev = *it; int count = 1; /* Collect neighbouring rows into one chunk */ ++it; while (it != sortedRows.rend()) { if (*it == prev - 1) { prev = *it; ++count; ++it; } else break; } Gui::cmdAppObjectArgs(sheet, "insertRows('%s', %d)", rowName(prev).c_str(), count); } Gui::Command::commitCommand(); Gui::Command::doCommand(Gui::Command::Doc, "App.ActiveDocument.recompute()"); } void SheetTableView::insertRowsAfter() { assert(sheet != 0); const auto rows = selectionModel()->selectedRows(); const auto & [min, max] = selectedMinMaxRows(rows); assert(max - min == rows.size() - 1); Q_UNUSED(min) Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Insert rows")); Gui::cmdAppObjectArgs(sheet, "insertRows('%s', %d)", rowName(max + 1).c_str(), rows.size()); Gui::Command::commitCommand(); Gui::Command::doCommand(Gui::Command::Doc, "App.ActiveDocument.recompute()"); } void SheetTableView::removeRows() { assert(sheet != 0); QModelIndexList rows = selectionModel()->selectedRows(); std::vector sortedRows; /* Make sure rows are sorted in descending order */ for (QModelIndexList::const_iterator it = rows.begin(); it != rows.end(); ++it) sortedRows.push_back(it->row()); std::sort(sortedRows.begin(), sortedRows.end(), std::greater()); /* Remove rows */ Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Remove rows")); for (std::vector::const_iterator it = sortedRows.begin(); it != sortedRows.end(); ++it) { Gui::cmdAppObjectArgs(sheet, "removeRows('%s', %d)", rowName(*it).c_str(), 1); } Gui::Command::commitCommand(); Gui::Command::doCommand(Gui::Command::Doc, "App.ActiveDocument.recompute()"); } void SheetTableView::insertColumns() { assert(sheet != 0); QModelIndexList cols = selectionModel()->selectedColumns(); std::vector sortedColumns; /* Make sure rows are sorted in ascending order */ for (QModelIndexList::const_iterator it = cols.begin(); it != cols.end(); ++it) sortedColumns.push_back(it->column()); std::sort(sortedColumns.begin(), sortedColumns.end()); /* Insert columns */ Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Insert columns")); std::vector::const_reverse_iterator it = sortedColumns.rbegin(); while (it != sortedColumns.rend()) { int prev = *it; int count = 1; /* Collect neighbouring columns into one chunk */ ++it; while (it != sortedColumns.rend()) { if (*it == prev - 1) { prev = *it; ++count; ++it; } else break; } Gui::cmdAppObjectArgs(sheet, "insertColumns('%s', %d)", columnName(prev).c_str(), count); } Gui::Command::commitCommand(); Gui::Command::doCommand(Gui::Command::Doc, "App.ActiveDocument.recompute()"); } void SheetTableView::insertColumnsAfter() { assert(sheet != 0); const auto columns = selectionModel()->selectedColumns(); const auto& [min, max] = selectedMinMaxColumns(columns); assert(max - min == columns.size() - 1); Q_UNUSED(min) Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Insert columns")); Gui::cmdAppObjectArgs(sheet, "insertColumns('%s', %d)", columnName(max + 1).c_str(), columns.size()); Gui::Command::commitCommand(); Gui::Command::doCommand(Gui::Command::Doc, "App.ActiveDocument.recompute()"); } void SheetTableView::removeColumns() { assert(sheet != 0); QModelIndexList cols = selectionModel()->selectedColumns(); std::vector sortedColumns; /* Make sure rows are sorted in descending order */ for (QModelIndexList::const_iterator it = cols.begin(); it != cols.end(); ++it) sortedColumns.push_back(it->column()); std::sort(sortedColumns.begin(), sortedColumns.end(), std::greater()); /* Remove columns */ Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Remove rows")); for (std::vector::const_iterator it = sortedColumns.begin(); it != sortedColumns.end(); ++it) Gui::cmdAppObjectArgs(sheet, "removeColumns('%s', %d)", columnName(*it).c_str(), 1); Gui::Command::commitCommand(); Gui::Command::doCommand(Gui::Command::Doc, "App.ActiveDocument.recompute()"); } SheetTableView::~SheetTableView() { } void SheetTableView::updateCellSpan(CellAddress address) { int rows, cols; sheet->getSpans(address, rows, cols); if (rows != rowSpan(address.row(), address.col()) || cols != columnSpan(address.row(), address.col())) setSpan(address.row(), address.col(), rows, cols); } void SheetTableView::setSheet(Sheet* _sheet) { sheet = _sheet; cellSpanChangedConnection = sheet->cellSpanChanged.connect(bind(&SheetTableView::updateCellSpan, this, bp::_1)); // Update row and column spans std::vector usedCells = sheet->getUsedCells(); for (std::vector::const_iterator i = usedCells.begin(); i != usedCells.end(); ++i) { CellAddress address(*i); if (sheet->isMergedCell(address)) updateCellSpan(address); } // Update column widths and row height std::map columWidths = sheet->getColumnWidths(); for (std::map::const_iterator i = columWidths.begin(); i != columWidths.end(); ++i) { int newSize = i->second; if (newSize > 0 && horizontalHeader()->sectionSize(i->first) != newSize) setColumnWidth(i->first, newSize); } std::map rowHeights = sheet->getRowHeights(); for (std::map::const_iterator i = rowHeights.begin(); i != rowHeights.end(); ++i) { int newSize = i->second; if (newSize > 0 && verticalHeader()->sectionSize(i->first) != newSize) setRowHeight(i->first, newSize); } } void SheetTableView::commitData(QWidget* editor) { QTableView::commitData(editor); } bool SheetTableView::edit(const QModelIndex& index, EditTrigger trigger, QEvent* event) { if (trigger & (QAbstractItemView::DoubleClicked | QAbstractItemView::AnyKeyPressed | QAbstractItemView::EditKeyPressed)) currentEditIndex = index; return QTableView::edit(index, trigger, event); } bool SheetTableView::event(QEvent* event) { if (event && event->type() == QEvent::KeyPress && this->hasFocus()) { // If this widget has focus, look for keyboard events that represent movement shortcuts // and handle them. QKeyEvent* kevent = static_cast(event); switch (kevent->key()) { case Qt::Key_Return: [[fallthrough]]; case Qt::Key_Enter: [[fallthrough]]; case Qt::Key_Home: [[fallthrough]]; case Qt::Key_End: [[fallthrough]]; case Qt::Key_Left: [[fallthrough]]; case Qt::Key_Right: [[fallthrough]]; case Qt::Key_Up: [[fallthrough]]; case Qt::Key_Down: [[fallthrough]]; case Qt::Key_Tab: [[fallthrough]]; case Qt::Key_Backtab: finishEditWithMove(kevent->key(), kevent->modifiers(), true); return true; // Also handle the delete key here: case Qt::Key_Delete: deleteSelection(); return true; case Qt::Key_Escape: sheet->setCopyOrCutRanges({}); return true; default: break; } if (kevent->matches(QKeySequence::Cut)) { cutSelection(); return true; } else if (kevent->matches(QKeySequence::Copy)) { copySelection(); return true; } else if (kevent->matches(QKeySequence::Paste)) { pasteClipboard(); return true; } } else if (event && event->type() == QEvent::ShortcutOverride) { QKeyEvent * kevent = static_cast(event); if (kevent->modifiers() == Qt::NoModifier || kevent->modifiers() == Qt::ShiftModifier || kevent->modifiers() == Qt::KeypadModifier) { switch (kevent->key()) { case Qt::Key_Return: [[fallthrough]]; case Qt::Key_Enter: [[fallthrough]]; case Qt::Key_Delete: [[fallthrough]]; case Qt::Key_Home: [[fallthrough]]; case Qt::Key_End: [[fallthrough]]; case Qt::Key_Backspace: [[fallthrough]]; case Qt::Key_Left: [[fallthrough]]; case Qt::Key_Right: [[fallthrough]]; case Qt::Key_Up: [[fallthrough]]; case Qt::Key_Down: [[fallthrough]]; case Qt::Key_Tab: kevent->accept(); break; default: break; } if (kevent->key() < Qt::Key_Escape) { kevent->accept(); } } if (kevent->matches(QKeySequence::Cut)) { kevent->accept(); } else if (kevent->matches(QKeySequence::Copy)) { kevent->accept(); } else if (kevent->matches(QKeySequence::Paste)) { kevent->accept(); } } return QTableView::event(event); } void SheetTableView::deleteSelection() { QModelIndexList selection = selectionModel()->selectedIndexes(); if (selection.size() > 0) { Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Clear cell(s)")); std::vector ranges = selectedRanges(); std::vector::const_iterator i = ranges.begin(); for (; i != ranges.end(); ++i) { Gui::Command::doCommand(Gui::Command::Doc,"App.ActiveDocument.%s.clear('%s')", sheet->getNameInDocument(), i->rangeString().c_str()); } Gui::Command::doCommand(Gui::Command::Doc, "App.ActiveDocument.recompute()"); Gui::Command::commitCommand(); } } static const QLatin1String _SheetMime("application/x-fc-spreadsheet"); void SheetTableView::copySelection() { _copySelection(selectedRanges(), true); } void SheetTableView::_copySelection(const std::vector &ranges, bool copy) { int minRow = INT_MAX; int maxRow = 0; int minCol = INT_MAX; int maxCol = 0; for (auto &range : ranges) { minRow = std::min(minRow, range.from().row()); maxRow = std::max(maxRow, range.to().row()); minCol = std::min(minCol, range.from().col()); maxCol = std::max(maxCol, range.to().col()); } QString selectedText; for (int i=minRow; i<=maxRow; i++) { for (int j=minCol; j<=maxCol; j++) { QModelIndex index = model()->index(i,j); QString cell = index.data(Qt::EditRole).toString(); if (j < maxCol) cell.append(QChar::fromLatin1('\t')); selectedText += cell; } if (i < maxRow) selectedText.append(QChar::fromLatin1('\n')); } Base::StringWriter writer; sheet->getCells()->copyCells(writer,ranges); QMimeData *mime = new QMimeData(); mime->setText(selectedText); mime->setData(_SheetMime,QByteArray(writer.getString().c_str())); QApplication::clipboard()->setMimeData(mime); sheet->setCopyOrCutRanges(std::move(ranges), copy); } void SheetTableView::cutSelection() { _copySelection(selectedRanges(), false); } void SheetTableView::pasteClipboard() { App::AutoTransaction committer("Paste cell"); try { bool copy = true; auto ranges = sheet->getCopyOrCutRange(copy); if(ranges.empty()) { copy = false; ranges = sheet->getCopyOrCutRange(copy); } if(ranges.size()) _copySelection(ranges, copy); const QMimeData* mimeData = QApplication::clipboard()->mimeData(); if(!mimeData || !mimeData->hasText()) return; if(!copy) { for(auto range : ranges) { do { sheet->clear(*range); } while (range.next()); } } ranges = selectedRanges(); if(ranges.empty()) return; Range range = ranges.back(); if (!mimeData->hasFormat(_SheetMime)) { CellAddress current = range.from(); QStringList cells; QString text = mimeData->text(); int i=0; for (auto it : text.split(QLatin1Char('\n'))) { QStringList cols = it.split(QLatin1Char('\t')); int j=0; for (auto jt : cols) { QModelIndex index = model()->index(current.row()+i, current.col()+j); model()->setData(index, jt); j++; } i++; } }else{ QByteArray res = mimeData->data(_SheetMime); Base::ByteArrayIStreambuf buf(res); std::istream in(0); in.rdbuf(&buf); Base::XMLReader reader("", in); sheet->getCells()->pasteCells(reader,range); } GetApplication().getActiveDocument()->recompute(); }catch(Base::Exception &e) { e.ReportException(); QMessageBox::critical(Gui::getMainWindow(), QObject::tr("Copy & Paste failed"), QString::fromLatin1(e.what())); return; } clearSelection(); } void SheetTableView::finishEditWithMove(int keyPressed, Qt::KeyboardModifiers modifiers, bool handleTabMotion) { // A utility lambda for finding the beginning and ending of data regions auto scanForRegionBoundary = [this](int& r, int& c, int dr, int dc) { auto startAddress = CellAddress(r, c); auto startCell = sheet->getCell(startAddress); bool startedAtEmptyCell = startCell ? !startCell->isUsed() : true; const int maxRow = this->model()->rowCount() - 1; const int maxCol = this->model()->columnCount() - 1; while (c + dc >= 0 && r + dr >= 0 && c + dc <= maxCol && r + dr <= maxRow) { r += dr; c += dc; auto cell = sheet->getCell(CellAddress(r, c)); auto cellIsEmpty = cell ? !cell->isUsed() : true; if (cellIsEmpty && !startedAtEmptyCell) { // Don't stop at the empty cell, stop at the last non-empty cell r -= dr; c -= dc; break; } else if (!cellIsEmpty && startedAtEmptyCell) { break; } } if (r == startAddress.row() && c == startAddress.col()) { // Always move at least one cell: r += dr; c += dc; } r = std::max(0, std::min(r, maxRow)); c = std::max(0, std::min(c, maxCol)); }; int targetRow = currentIndex().row(); int targetColumn = currentIndex().column(); int colSpan; int rowSpan; sheet->getSpans(CellAddress(targetRow, targetColumn), rowSpan, colSpan); switch (keyPressed) { case Qt::Key_Return: case Qt::Key_Enter: if (modifiers == Qt::NoModifier) { targetRow += rowSpan; targetColumn -= tabCounter; } else if (modifiers == Qt::ShiftModifier) { targetRow -= 1; targetColumn -= tabCounter; } else { // For an unrecognized modifier, just go down targetRow += rowSpan; } tabCounter = 0; break; case Qt::Key_Home: // Home: row 1, same column // Ctrl-Home: row 1, column 1 targetRow = 0; if (modifiers == Qt::ControlModifier) targetColumn = 0; tabCounter = 0; break; case Qt::Key_End: { // End should take you to the last occupied cell in the current column // Ctrl-End takes you to the last cell in the sheet auto usedCells = sheet->getCells()->getUsedCells(); for (const auto& cell : usedCells) { if (modifiers == Qt::NoModifier) { if (cell.col() == targetColumn) targetRow = std::max(targetRow, cell.row()); } else if (modifiers == Qt::ControlModifier) { targetRow = std::max(targetRow, cell.row()); targetColumn = std::max(targetColumn, cell.col()); } } tabCounter = 0; break; } case Qt::Key_Left: if (targetColumn == 0) break; // Nothing to do, we're already in the first column if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) targetColumn--; else if (modifiers == Qt::ControlModifier || modifiers == (Qt::ControlModifier | Qt::ShiftModifier)) scanForRegionBoundary(targetRow, targetColumn, 0, -1); else targetColumn--; //Unrecognized modifier combination: default to just moving one cell tabCounter = 0; break; case Qt::Key_Right: if (targetColumn >= this->model()->columnCount() - 1) break; // Nothing to do, we're already in the last column if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) targetColumn += colSpan; else if (modifiers == Qt::ControlModifier || modifiers == (Qt::ControlModifier | Qt::ShiftModifier)) scanForRegionBoundary(targetRow, targetColumn, 0, 1); else targetColumn += colSpan; //Unrecognized modifier combination: default to just moving one cell tabCounter = 0; break; case Qt::Key_Up: if (targetRow == 0) break; // Nothing to do, we're already in the first column if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) targetRow--; else if (modifiers == Qt::ControlModifier || modifiers == (Qt::ControlModifier | Qt::ShiftModifier)) scanForRegionBoundary(targetRow, targetColumn, -1, 0); else targetRow--; //Unrecognized modifier combination: default to just moving one cell tabCounter = 0; break; case Qt::Key_Down: if (targetRow >= this->model()->rowCount() - 1) break; // Nothing to do, we're already in the last row if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) targetRow += rowSpan; else if (modifiers == Qt::ControlModifier || modifiers == (Qt::ControlModifier | Qt::ShiftModifier)) scanForRegionBoundary(targetRow, targetColumn, 1, 0); else targetRow += rowSpan; //Unrecognized modifier combination: default to just moving one cell tabCounter = 0; break; case Qt::Key_Tab: if (modifiers == Qt::NoModifier) { tabCounter++; if (handleTabMotion) targetColumn += colSpan; } else if (modifiers == Qt::ShiftModifier) { tabCounter = 0; if (handleTabMotion) targetColumn--; } break; case Qt::Key_Backtab: if (modifiers == Qt::NoModifier) { targetColumn--; } tabCounter = 0; break; default: break; } if (this->sheet->isMergedCell(CellAddress(targetRow, targetColumn))) { auto anchor = this->sheet->getAnchor(CellAddress(targetRow, targetColumn)); targetRow = anchor.row(); targetColumn = anchor.col(); } // Overflow/underflow protection: const int maxRow = this->model()->rowCount() - 1; const int maxCol = this->model()->columnCount() - 1; targetRow = std::max(0, std::min(targetRow, maxRow)); targetColumn = std::max(0, std::min(targetColumn, maxCol)); if (!(modifiers & Qt::ShiftModifier) || keyPressed == Qt::Key_Tab || keyPressed == Qt::Key_Enter || keyPressed == Qt::Key_Return) { // We have to use this method so that Ctrl-modifier combinations don't result in multiple selection this->selectionModel()->setCurrentIndex(model()->index(targetRow, targetColumn), QItemSelectionModel::ClearAndSelect); } else if (modifiers & Qt::ShiftModifier) { // With shift down, this motion becomes a block selection command, rather than just simple motion: ModifyBlockSelection(targetRow, targetColumn); } } void SheetTableView::ModifyBlockSelection(int targetRow, int targetCol) { int startingRow = currentIndex().row(); int startingCol = currentIndex().column(); // Get the current block selection size: auto selection = this->selectionModel()->selection(); for (const auto& range : selection) { if (range.contains(currentIndex())) { // This range contains the current cell, so it's the one we're going to modify (assuming we're at one of the corners) int rangeMinRow = range.top(); int rangeMaxRow = range.bottom(); int rangeMinCol = range.left(); int rangeMaxCol = range.right(); if ((startingRow == rangeMinRow || startingRow == rangeMaxRow) && (startingCol == rangeMinCol || startingCol == rangeMaxCol)) { if (range.contains(model()->index(targetRow, targetCol))) { // If the range already contains the target cell, then we're making the range smaller if (startingRow == rangeMinRow) rangeMinRow = targetRow; if (startingRow == rangeMaxRow) rangeMaxRow = targetRow; if (startingCol == rangeMinCol) rangeMinCol = targetCol; if (startingCol == rangeMaxCol) rangeMaxCol = targetCol; } else { // We're making the range bigger rangeMinRow = std::min(rangeMinRow, targetRow); rangeMaxRow = std::max(rangeMaxRow, targetRow); rangeMinCol = std::min(rangeMinCol, targetCol); rangeMaxCol = std::max(rangeMaxCol, targetCol); } QItemSelection oldRange(range.topLeft(), range.bottomRight()); this->selectionModel()->select(oldRange, QItemSelectionModel::Deselect); QItemSelection newRange(model()->index(rangeMinRow, rangeMinCol), model()->index(rangeMaxRow, rangeMaxCol)); this->selectionModel()->select(newRange, QItemSelectionModel::Select); } break; } } this->selectionModel()->setCurrentIndex(model()->index(targetRow, targetCol), QItemSelectionModel::Current); } void SheetTableView::mergeCells() { Gui::Application::Instance->commandManager().runCommandByName("Spreadsheet_MergeCells"); } void SheetTableView::splitCell() { Gui::Application::Instance->commandManager().runCommandByName("Spreadsheet_SplitCell"); } void SheetTableView::closeEditor(QWidget * editor, QAbstractItemDelegate::EndEditHint hint) { QTableView::closeEditor(editor, hint); } void SheetTableView::mousePressEvent(QMouseEvent* event) { tabCounter = 0; QTableView::mousePressEvent(event); } void SheetTableView::edit ( const QModelIndex & index ) { currentEditIndex = index; QTableView::edit(index); } void SheetTableView::contextMenuEvent(QContextMenuEvent *) { const QMimeData* mimeData = QApplication::clipboard()->mimeData(); if (!selectionModel()->hasSelection()) { actionCut->setEnabled(false); actionCopy->setEnabled(false); actionDel->setEnabled(false); actionPaste->setEnabled(false); actionSplit->setEnabled(false); actionMerge->setEnabled(false); } else { actionPaste->setEnabled(mimeData && (mimeData->hasText() || mimeData->hasText())); actionCut->setEnabled(true); actionCopy->setEnabled(true); actionDel->setEnabled(true); actionSplit->setEnabled(true); actionMerge->setEnabled(true); } auto ranges = selectedRanges(); actionBind->setEnabled(ranges.size()>=1 && ranges.size()<=2); contextMenu->exec(QCursor::pos()); } QString SheetTableView::toHtml() const { std::set cells = sheet->getCells()->getUsedCells(); int rowCount = 1; int colCount = 1; for (const auto& it : cells) { rowCount = std::max(rowCount, it.row()); colCount = std::max(colCount, it.col()); } std::unique_ptr doc(new QTextDocument); doc->setDocumentMargin(10); QTextCursor cursor(doc.get()); cursor.movePosition(QTextCursor::Start); QTextTableFormat tableFormat; tableFormat.setCellSpacing(0.0); tableFormat.setCellPadding(2.0); QVector constraints; for (int col = 0; col < colCount + 1; col++) { constraints.append(QTextLength(QTextLength::FixedLength, sheet->getColumnWidth(col))); } constraints.prepend(QTextLength(QTextLength::FixedLength, 30.0)); tableFormat.setColumnWidthConstraints(constraints); QTextCharFormat boldFormat; QFont boldFont = boldFormat.font(); boldFont.setBold(true); boldFormat.setFont(boldFont); QColor bgColor; bgColor.setNamedColor(QLatin1String("#f0f0f0")); QTextCharFormat bgFormat; bgFormat.setBackground(QBrush(bgColor)); QTextTable *table = cursor.insertTable(rowCount + 2, colCount + 2, tableFormat); // The header cells of the rows for (int row = 0; row < rowCount + 1; row++) { QTextTableCell headerCell = table->cellAt(row+1, 0); headerCell.setFormat(bgFormat); QTextCursor headerCellCursor = headerCell.firstCursorPosition(); QString data = model()->headerData(row, Qt::Vertical).toString(); headerCellCursor.insertText(data, boldFormat); } // The header cells of the columns for (int col = 0; col < colCount + 1; col++) { QTextTableCell headerCell = table->cellAt(0, col+1); headerCell.setFormat(bgFormat); QTextCursor headerCellCursor = headerCell.firstCursorPosition(); QTextBlockFormat blockFormat = headerCellCursor.blockFormat(); blockFormat.setAlignment(Qt::AlignHCenter); headerCellCursor.setBlockFormat(blockFormat); QString data = model()->headerData(col, Qt::Horizontal).toString(); headerCellCursor.insertText(data, boldFormat); } // The cells for (const auto& it : cells) { if (sheet->isMergedCell(it)) { int rows, cols; sheet->getSpans(it, rows, cols); table->mergeCells(it.row() + 1, it.col() + 1, rows, cols); } QModelIndex index = model()->index(it.row(), it.col()); QTextCharFormat cellFormat; QTextTableCell cell = table->cellAt(it.row() + 1, it.col() + 1); // font QVariant font = model()->data(index, Qt::FontRole); if (font.isValid()) { cellFormat.setFont(font.value()); } // foreground QVariant fgColor = model()->data(index, Qt::ForegroundRole); if (fgColor.isValid()) { cellFormat.setForeground(QBrush(fgColor.value())); } // background QVariant cbgClor = model()->data(index, Qt::BackgroundRole); if (cbgClor.isValid()) { QTextCharFormat bgFormat; bgFormat.setBackground(QBrush(cbgClor.value())); cell.setFormat(bgFormat); } QTextCursor cellCursor = cell.firstCursorPosition(); // alignment QVariant align = model()->data(index, Qt::TextAlignmentRole); if (align.isValid()) { Qt::Alignment alignment = static_cast(align.toInt()); QTextBlockFormat blockFormat = cellCursor.blockFormat(); blockFormat.setAlignment(alignment); cellCursor.setBlockFormat(blockFormat); // This doesn't seem to have any effect on single cells but works if several // cells are merged QTextCharFormat::VerticalAlignment valign = QTextCharFormat::AlignMiddle; QTextCharFormat format = cell.format(); if (alignment & Qt::AlignTop) { valign = QTextCharFormat::AlignTop; } else if (alignment & Qt::AlignBottom) { valign = QTextCharFormat::AlignBottom; } format.setVerticalAlignment(valign); cell.setFormat(format); } // text QString data = model()->data(index).toString().simplified(); cellCursor.insertText(data, cellFormat); } cursor.movePosition(QTextCursor::End); cursor.insertBlock(); return doc->toHtml(); } #include "moc_SheetTableView.cpp"