1263 lines
45 KiB
C++
1263 lines
45 KiB
C++
/***************************************************************************
|
|
* Copyright (c) 2015 Eivind Kvedalen <eivind@kvedalen.name> *
|
|
* *
|
|
* 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 <limits>
|
|
#include <QAction>
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QKeyEvent>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QMimeData>
|
|
#endif
|
|
#include <QTextTableCell>
|
|
|
|
#include <App/Application.h>
|
|
#include <App/AutoTransaction.h>
|
|
#include <App/Document.h>
|
|
#include <App/Range.h>
|
|
#include <Base/Reader.h>
|
|
#include <Base/Stream.h>
|
|
#include <Base/Writer.h>
|
|
#include <Gui/Application.h>
|
|
#include <Gui/CommandT.h>
|
|
#include <Gui/MainWindow.h>
|
|
#include <Mod/Spreadsheet/App/Cell.h>
|
|
|
|
#include "DlgBindSheet.h"
|
|
#include "DlgSheetConf.h"
|
|
#include "LineEdit.h"
|
|
#include "PropertiesDialog.h"
|
|
#include "SheetTableView.h"
|
|
|
|
|
|
using namespace SpreadsheetGui;
|
|
using namespace Spreadsheet;
|
|
using namespace App;
|
|
namespace sp = std::placeholders;
|
|
|
|
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<QContextMenuEvent*>(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<int, int> selectedMinMaxRows(QModelIndexList list)
|
|
{
|
|
int min = std::numeric_limits<int>::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<int, int> selectedMinMaxColumns(QModelIndexList list)
|
|
{
|
|
int min = std::numeric_limits<int>::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 row(s)", "", 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());
|
|
});
|
|
|
|
actionProperties = new QAction(tr("Properties..."), this);
|
|
addAction(actionProperties);
|
|
|
|
horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
verticalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
verticalHeader()->setDefaultAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
|
|
|
|
contextMenu.addAction(actionProperties);
|
|
connect(actionProperties, &QAction::triggered, this, &SheetTableView::cellProperties);
|
|
|
|
contextMenu.addSeparator();
|
|
actionRecompute = new QAction(tr("Recompute"), this);
|
|
connect(actionRecompute, &QAction::triggered, this, &SheetTableView::onRecompute);
|
|
contextMenu.addAction(actionRecompute);
|
|
|
|
actionBind = new QAction(tr("Bind..."), this);
|
|
connect(actionBind, &QAction::triggered, this, &SheetTableView::onBind);
|
|
contextMenu.addAction(actionBind);
|
|
|
|
actionConf = new QAction(tr("Configuration table..."), this);
|
|
connect(actionConf, &QAction::triggered, this, &SheetTableView::onConfSetup);
|
|
contextMenu.addAction(actionConf);
|
|
|
|
horizontalHeader()->addAction(actionBind);
|
|
verticalHeader()->addAction(actionBind);
|
|
|
|
contextMenu.addSeparator();
|
|
actionMerge = contextMenu.addAction(tr("Merge cells"));
|
|
connect(actionMerge, &QAction::triggered, this, &SheetTableView::mergeCells);
|
|
actionSplit = contextMenu.addAction(tr("Split cells"));
|
|
connect(actionSplit, &QAction::triggered, this, &SheetTableView::splitCell);
|
|
|
|
contextMenu.addSeparator();
|
|
actionCut = contextMenu.addAction(tr("Cut"));
|
|
connect(actionCut, &QAction::triggered, this, &SheetTableView::cutSelection);
|
|
actionCopy = contextMenu.addAction(tr("Copy"));
|
|
connect(actionCopy, &QAction::triggered, this, &SheetTableView::copySelection);
|
|
actionPaste = contextMenu.addAction(tr("Paste"));
|
|
connect(actionPaste, &QAction::triggered, this, &SheetTableView::pasteClipboard);
|
|
actionDel = contextMenu.addAction(tr("Delete"));
|
|
connect(actionDel, &QAction::triggered, this, &SheetTableView::deleteSelection);
|
|
|
|
setTabKeyNavigation(false);
|
|
|
|
timer.setSingleShot(true);
|
|
QObject::connect(&timer, &QTimer::timeout, [this]() {
|
|
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<Range> SheetTableView::selectedRanges() const
|
|
{
|
|
std::vector<Range> 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<int> 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<int>::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<int> 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<int> 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<int>::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<int> 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<std::string> 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<int, int> columWidths = sheet->getColumnWidths();
|
|
for (std::map<int, int>::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<int, int> rowHeights = sheet->getRowHeights();
|
|
for (std::map<int, int>::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<QKeyEvent*>(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<QKeyEvent*>(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 cells"));
|
|
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 cell(s)"));
|
|
std::vector<Range> ranges = selectedRanges();
|
|
std::vector<Range>::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<App::Range>& ranges, bool copy)
|
|
{
|
|
int minRow = std::numeric_limits<int>::max();
|
|
int maxRow = 0;
|
|
int minCol = std::numeric_limits<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 (!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("<memory>", 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<QTextDocument> 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<QTextLength> 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<QFont>());
|
|
}
|
|
|
|
// foreground
|
|
QVariant fgColor = model()->data(index, Qt::ForegroundRole);
|
|
if (fgColor.isValid()) {
|
|
cellFormat.setForeground(QBrush(fgColor.value<QColor>()));
|
|
}
|
|
|
|
// background
|
|
QVariant cbgClor = model()->data(index, Qt::BackgroundRole);
|
|
if (cbgClor.isValid()) {
|
|
QTextCharFormat bgFormat;
|
|
bgFormat.setBackground(QBrush(cbgClor.value<QColor>()));
|
|
cell.setFormat(bgFormat);
|
|
}
|
|
|
|
QTextCursor cellCursor = cell.firstCursorPosition();
|
|
|
|
// alignment
|
|
QVariant align = model()->data(index, Qt::TextAlignmentRole);
|
|
if (align.isValid()) {
|
|
Qt::Alignment alignment = static_cast<Qt::Alignment>(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"
|