| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | #include <limits> |
| | #include <QAction> |
| | #include <QApplication> |
| | #include <QClipboard> |
| | #include <QKeyEvent> |
| | #include <QMenu> |
| | #include <QMessageBox> |
| | #include <QMimeData> |
| |
|
| | #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; |
| |
|
| | void SheetViewHeader::mouseMoveEvent(QMouseEvent* e) |
| | { |
| | |
| | |
| | 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<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) |
| | |
| | |
| | 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) |
| | |
| | |
| | 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<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 { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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) { |
| | |
| | |
| | |
| | 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()) { |
| | |
| | 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; |
| |
|
| | |
| | for (const auto& it : rows) { |
| | sortedRows.push_back(it.row()); |
| | } |
| | std::sort(sortedRows.begin(), sortedRows.end()); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | ++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; |
| |
|
| | |
| | for (const auto& it : rows) { |
| | sortedRows.push_back(it.row()); |
| | } |
| | std::sort(sortedRows.begin(), sortedRows.end(), std::greater<>()); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | for (const auto& it : cols) { |
| | sortedColumns.push_back(it.column()); |
| | } |
| | std::sort(sortedColumns.begin(), sortedColumns.end()); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | ++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; |
| |
|
| | |
| | for (const auto& it : cols) { |
| | sortedColumns.push_back(it.column()); |
| | } |
| | std::sort(sortedColumns.begin(), sortedColumns.end(), std::greater<>()); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | 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); |
| | }); |
| |
|
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | 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()) { |
| | |
| | |
| | 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 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<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) |
| | { |
| | |
| | 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) { |
| | |
| | r -= dr; |
| | c -= dc; |
| | break; |
| | } |
| | else if (!cellIsEmpty && startedAtEmptyCell) { |
| | break; |
| | } |
| | } |
| | if (r == startAddress.row() && c == startAddress.col()) { |
| | |
| | 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 { |
| | |
| | targetRow += rowSpan; |
| | } |
| | tabCounter = 0; |
| | break; |
| |
|
| | case Qt::Key_Home: |
| | |
| | |
| | targetRow = 0; |
| | if (modifiers == Qt::ControlModifier) { |
| | targetColumn = 0; |
| | } |
| | tabCounter = 0; |
| | break; |
| |
|
| | case Qt::Key_End: { |
| | |
| | |
| | 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; |
| | } |
| | 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--; |
| | |
| | } |
| | tabCounter = 0; |
| | break; |
| | case Qt::Key_Right: |
| | if (targetColumn >= this->model()->columnCount() - 1) { |
| | break; |
| | } |
| | 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; |
| | |
| | } |
| | tabCounter = 0; |
| | break; |
| | case Qt::Key_Up: |
| | if (targetRow == 0) { |
| | break; |
| | } |
| | 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--; |
| | } |
| | tabCounter = 0; |
| | break; |
| | case Qt::Key_Down: |
| | if (targetRow >= this->model()->rowCount() - 1) { |
| | break; |
| | } |
| | 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; |
| | |
| | } |
| | 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(); |
| | } |
| |
|
| | |
| | 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) { |
| | |
| | |
| | this->selectionModel()->setCurrentIndex( |
| | model()->index(targetRow, targetColumn), |
| | QItemSelectionModel::ClearAndSelect |
| | ); |
| | } |
| | else if (modifiers & Qt::ShiftModifier) { |
| | |
| | |
| | ModifyBlockSelection(targetRow, targetColumn); |
| | } |
| | } |
| |
|
| | void SheetTableView::ModifyBlockSelection(int targetRow, int targetCol) |
| | { |
| | int startingRow = currentIndex().row(); |
| | int startingCol = currentIndex().column(); |
| |
|
| | |
| | auto selection = this->selectionModel()->selection(); |
| | for (const auto& range : selection) { |
| | if (range.contains(currentIndex())) { |
| | |
| | |
| | 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 (startingRow == rangeMinRow) { |
| | rangeMinRow = targetRow; |
| | } |
| | if (startingRow == rangeMaxRow) { |
| | rangeMaxRow = targetRow; |
| | } |
| | if (startingCol == rangeMinCol) { |
| | rangeMinCol = targetCol; |
| | } |
| | if (startingCol == rangeMaxCol) { |
| | rangeMaxCol = targetCol; |
| | } |
| | } |
| | else { |
| | |
| | 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); |
| |
|
| | |
| | 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); |
| | } |
| |
|
| | |
| | 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); |
| | } |
| |
|
| | |
| | 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); |
| |
|
| | |
| | QVariant font = model()->data(index, Qt::FontRole); |
| | if (font.isValid()) { |
| | cellFormat.setFont(font.value<QFont>()); |
| | } |
| |
|
| | |
| | QVariant fgColor = model()->data(index, Qt::ForegroundRole); |
| | if (fgColor.isValid()) { |
| | cellFormat.setForeground(QBrush(fgColor.value<QColor>())); |
| | } |
| |
|
| | |
| | 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(); |
| |
|
| | |
| | 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); |
| |
|
| | |
| | |
| | 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); |
| | } |
| |
|
| | |
| | QString data = model()->data(index).toString().simplified(); |
| | cellCursor.insertText(data, cellFormat); |
| | } |
| |
|
| | cursor.movePosition(QTextCursor::End); |
| | cursor.insertBlock(); |
| | return doc->toHtml(); |
| | } |
| |
|
| | #include "moc_SheetTableView.cpp" |
| |
|