// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2015 Eivind Kvedalen * * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DlgBindSheet.h" #include "DlgSheetConf.h" #include "LineEdit.h" #include "PropertiesDialog.h" #include "SheetTableView.h" using namespace SpreadsheetGui; using namespace Spreadsheet; using namespace App; void SheetViewHeader::mouseMoveEvent(QMouseEvent* e) { // for some reason QWidget::setCursor() has no effect in QGraphicsView // therefore we resort to QGraphicsItem::setCursor const QCursor currentCursor = this->cursor(); QHeaderView::mouseMoveEvent(e); const QCursor newerCursor = this->cursor(); if (newerCursor != currentCursor) { Q_EMIT cursorChanged(newerCursor); } } void SheetViewHeader::mouseReleaseEvent(QMouseEvent* event) { QHeaderView::mouseReleaseEvent(event); Q_EMIT 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(nullptr) , 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) { Q_UNUSED(point) QMenu menu {nullptr}; 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, &QAction::triggered, this, &SheetTableView::insertRows); if (max < model()->rowCount() - 1) { auto insertAfter = menu.addAction(tr("Insert %n Row(s) Below", "", selection.size())); connect(insertAfter, &QAction::triggered, this, &SheetTableView::insertRowsAfter); } } else { auto insert = menu.addAction(tr("Insert %n Non-Contiguous Rows", "", selection.size())); connect(insert, &QAction::triggered, this, &SheetTableView::insertRows); } auto remove = menu.addAction(tr("Remove Rows", "", selection.size())); connect(remove, &QAction::triggered, this, &SheetTableView::removeRows); menu.exec(QCursor::pos()); }); connect(horizontalHeader(), &QWidget::customContextMenuRequested, [this](const QPoint& point) { Q_UNUSED(point) QMenu menu {nullptr}; 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, &QAction::triggered, this, &SheetTableView::insertColumns); if (max < model()->columnCount() - 1) { auto insertAfter = menu.addAction( tr("Insert %n Column(s) Right", "", selection.size()) ); connect(insertAfter, &QAction::triggered, this, &SheetTableView::insertColumnsAfter); } } else { auto insert = menu.addAction(tr("Insert %n Non-Contiguous Columns", "", selection.size())); connect(insert, &QAction::triggered, this, &SheetTableView::insertColumns); } auto remove = menu.addAction(tr("Remove Column(s)", "", selection.size())); connect(remove, &QAction::triggered, this, &SheetTableView::removeColumns); menu.exec(QCursor::pos()); }); auto createAction = [this](const char* iconPath, const QString& text, auto fun) { const QIcon icon {QString::fromLatin1(iconPath)}; auto act = new QAction(icon, text, this); connect(act, &QAction::triggered, this, fun); contextMenu.addAction(act); return act; }; actionProperties = createAction("", tr("Properties"), &SheetTableView::cellProperties); contextMenu.addSeparator(); actionRecompute = createAction(":/icons/view-refresh.svg", tr("Recompute"), &SheetTableView::onRecompute); actionBind = createAction("", tr("Bind…"), &SheetTableView::onBind); actionConf = createAction("", tr("Configuration Table"), &SheetTableView::onConfSetup); contextMenu.addSeparator(); actionMerge = createAction( ":/icons/SpreadsheetMergeCells.svg", tr("Merge Cells"), &SheetTableView::mergeCells ); actionSplit = createAction( ":/icons/SpreadsheetSplitCell.svg", tr("Split Cell"), &SheetTableView::splitCell ); contextMenu.addSeparator(); actionCut = createAction(":/icons/edit-cut.svg", tr("Cut"), &SheetTableView::cutSelection); actionCopy = createAction(":/icons/edit-copy.svg", tr("Copy"), &SheetTableView::copySelection); actionPaste = createAction(":/icons/edit-paste.svg", tr("Paste"), &SheetTableView::pasteClipboard); actionDel = createAction(":/icons/edit-delete.svg", tr("Delete"), &SheetTableView::deleteSelection); addAction(actionProperties); horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); verticalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); verticalHeader()->setDefaultAlignment(Qt::AlignHCenter | Qt::AlignVCenter); horizontalHeader()->addAction(actionBind); verticalHeader()->addAction(actionBind); setTabKeyNavigation(false); timer.setSingleShot(true); QObject::connect(&timer, &QTimer::timeout, this, &SheetTableView::updateCellSpan); } 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.empty() && ranges.size() <= 2) { DlgBindSheet dlg {sheet, ranges}; dlg.exec(); } } void SheetTableView::onConfSetup() { auto ranges = selectedRanges(); if (ranges.empty()) { return; } DlgSheetConf dlg {sheet, ranges.back()}; dlg.exec(); } void SheetTableView::cellProperties() { PropertiesDialog dialog {sheet, selectedRanges()}; if (dialog.exec() == QDialog::Accepted) { dialog.apply(); } } std::vector SheetTableView::selectedRanges() const { std::vector result; if (!sheet->getCells()->hasSpan()) { for (const auto& sel : selectionModel()->selection()) { result.emplace_back(sel.top(), sel.left(), sel.bottom(), sel.right()); } } else { // If there is spanning cell, QItemSelection returned by // QTableView::selection() does not merge selected indices into ranges. // So we have to do it by ourselves. Qt records selection in the order // of column first and then row. // // Note that there will always be ambiguous cases with the available // information, where multiple user selected ranges are merged // together. For example, consecutive single column selections that // form a rectangle will be merged together, but single row selections // will not be merged. for (const auto& sel : selectionModel()->selection()) { if (!result.empty() && sel.bottom() == sel.top() && sel.right() == sel.left()) { auto& last = result.back(); if (last.colCount() == 1 && last.from().col() == sel.left() && sel.top() == last.to().row() + 1) { // This is the case of rectangle selection. We keep // accumulating the last column, and try to merge the // column to previous range whenever possible. last = Range(last.from(), CellAddress(sel.top(), sel.left())); if (result.size() > 1) { auto& secondLast = result[result.size() - 2]; if (secondLast.to().col() + 1 == last.to().col() && secondLast.from().row() == last.from().row() && secondLast.rowCount() == last.rowCount()) { secondLast = Range(secondLast.from(), last.to()); result.pop_back(); } } continue; } else if (last.rowCount() == 1 && last.from().row() == sel.top() && last.to().col() + 1 == sel.left()) { // This is the case of single row selection last = Range(last.from(), CellAddress(sel.top(), sel.left())); continue; } } result.emplace_back(sel.top(), sel.left(), sel.bottom(), sel.right()); } } return result; } QModelIndexList SheetTableView::selectedIndexesRaw() const { return selectedIndexes(); } void SheetTableView::insertRows() { assert(sheet); QModelIndexList rows = selectionModel()->selectedRows(); std::vector sortedRows; /* Make sure rows are sorted in ascending order */ for (const auto& it : rows) { 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); 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); QModelIndexList rows = selectionModel()->selectedRows(); std::vector sortedRows; /* Make sure rows are sorted in descending order */ for (const auto& it : rows) { 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 (const auto& it : sortedRows) { 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); QModelIndexList cols = selectionModel()->selectedColumns(); std::vector sortedColumns; /* Make sure rows are sorted in ascending order */ for (const auto& it : cols) { 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); 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); QModelIndexList cols = selectionModel()->selectedColumns(); std::vector sortedColumns; /* Make sure rows are sorted in descending order */ for (const auto& it : cols) { 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 (const auto& it : sortedColumns) { 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() = default; void SheetTableView::updateCellSpan() { int rows, cols; // Unspan first to avoid overlap for (const auto& addr : spanChanges) { if (rowSpan(addr.row(), addr.col()) > 1 || columnSpan(addr.row(), addr.col()) > 1) { setSpan(addr.row(), addr.col(), 1, 1); } } for (const auto& addr : spanChanges) { sheet->getSpans(addr, rows, cols); if (rows > 1 || cols > 1) { setSpan(addr.row(), addr.col(), rows, cols); } } spanChanges.clear(); } void SheetTableView::setSheet(Sheet* _sheet) { sheet = _sheet; cellSpanChangedConnection = sheet->cellSpanChanged.connect([&](const CellAddress& addr) { spanChanges.insert(addr); timer.start(10); }); // Update row and column spans std::vector usedCells = sheet->getUsedCells(); for (const auto& i : usedCells) { CellAddress address(i); if (sheet->isMergedCell(address)) { int rows, cols; sheet->getSpans(address, rows, cols); setSpan(address.row(), address.col(), rows, cols); } } // 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; case Qt::Key_Escape: sheet->setCopyOrCutRanges({}); return true; default: break; } if (kevent->matches(QKeySequence::Delete) || kevent->matches(QKeySequence::Backspace)) { deleteSelection(); } 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_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::Delete) || kevent->matches(QKeySequence::Backspace)) { 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(); } } else if (event && event->type() == QEvent::LanguageChange) { actionProperties->setText(tr("Properties…")); actionRecompute->setText(tr("Recompute")); actionConf->setText(tr("Configuration Table…")); actionMerge->setText(tr("Merge Cells")); actionSplit->setText(tr("Split Cell")); actionCopy->setText(tr("Copy")); actionPaste->setText(tr("Paste")); actionCut->setText(tr("Cut")); actionDel->setText(tr("Delete")); actionBind->setText(tr("Bind…")); } return QTableView::event(event); } void SheetTableView::deleteSelection() { QModelIndexList selection = selectionModel()->selectedIndexes(); if (!selection.empty()) { Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Clear Cells")); 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 = std::numeric_limits::max(); int maxRow = 0; int minCol = std::numeric_limits::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 (!cell.isEmpty() && cell.at(0) == QLatin1Char('\'')) { cell.remove(0, 1); } 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.empty()) { _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(); QString text = mimeData->text(); QStringList cells = text.split(QLatin1Char('\n')); int i = 0; for (const auto& it : cells) { QStringList cols = it.split(QLatin1Char('\t')); int j = 0; for (const 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(nullptr); 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()->getNonEmptyCells(); 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::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { Gui::getMainWindow()->updateActions(); QTableView::selectionChanged(selected, deselected); } 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()); actionCut->setEnabled(true); actionCopy->setEnabled(true); actionDel->setEnabled(true); actionSplit->setEnabled( selectedIndexesRaw().size() == 1 && sheet->isMergedCell(CellAddress(currentIndex().row(), currentIndex().column())) ); actionMerge->setEnabled(selectedIndexesRaw().size() > 1); } auto ranges = selectedRanges(); actionBind->setEnabled(!ranges.empty() && ranges.size() <= 2); contextMenu.exec(QCursor::pos()); } QString SheetTableView::toHtml() const { auto cells = sheet->getCells()->getNonEmptyCells(); int rowCount = 0; int colCount = 0; 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(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"