Files
sqlitebrowser/src/ExtendedTableWidget.cpp
Martin Kleusberg 50b48bc756 Avoid multiple error messages when deleting cell values
When multiple cells are selected pressing the Delete key tries to set
all of them to an empty string. In case of a unique constraint or
similar constraints this throws an error. This commit copies the
behaviour of the set to NULL menu action in that it aborts at the first
error to avoid multiple error messages.

See issue #2704.
2021-05-22 09:55:49 +02:00

1365 lines
57 KiB
C++

#include "ExtendedTableWidget.h"
#include "sqlitetablemodel.h"
#include "FilterTableHeader.h"
#include "sql/sqlitetypes.h"
#include "Settings.h"
#include "sqlitedb.h"
#include "CondFormat.h"
#include <QApplication>
#include <QClipboard>
#include <QMimeData>
#include <QKeySequence>
#include <QKeyEvent>
#include <QScrollBar>
#include <QHeaderView>
#include <QMessageBox>
#include <QBuffer>
#include <QMenu>
#include <QDateTime>
#include <QLineEdit>
#include <QPrinter>
#include <QPrintPreviewDialog>
#include <QTextDocument>
#include <QCompleter>
#include <QComboBox>
#include <QPainter>
#include <QShortcut>
#include <limits>
using BufferRow = std::vector<QByteArray>;
std::vector<BufferRow> ExtendedTableWidget::m_buffer;
QString ExtendedTableWidget::m_generatorStamp;
namespace
{
std::vector<BufferRow> parseClipboard(QString clipboard)
{
// Remove trailing line break from the clipboard text. This is necessary because some applications append an extra
// line break to the clipboard contents which we would then interpret as regular data, setting the first field of the
// first row after the paste area to NULL. One problem here is that this breaks for those cases where an empty line at
// the end of the selection is explicitly copied and the originating application doesn't add an extra line break. However,
// there are two reasons for favoring this way: 1) Spreadsheet applications seem to add an extra line break and they are
// probably the main source for pasted data, 2) Having to manually delete on extra field seems to be less problematic than
// having one extra field deleted without any warning.
if(clipboard.endsWith("\n"))
clipboard.chop(1);
if(clipboard.endsWith("\r"))
clipboard.chop(1);
// Make sure there is some data in the clipboard
std::vector<BufferRow> result;
if(clipboard.isEmpty())
return result;
result.push_back(BufferRow());
QRegExp re("(\"(?:[^\t\"]+|\"\"[^\"]*\"\")*)\"|(\t|\r?\n)");
int offset = 0;
int whitespace_offset = 0;
while (offset >= 0) {
QString text;
int pos = re.indexIn(clipboard, offset);
if (pos < 0) {
// insert everything that left
text = clipboard.mid(whitespace_offset);
if(QRegExp("\".*\"").exactMatch(text))
text = text.mid(1, text.length() - 2);
text.replace("\"\"", "\"");
result.back().push_back(text.toUtf8());
break;
}
if (re.pos(2) < 0) {
offset = pos + re.cap(1).length() + 1;
continue;
}
QString ws = re.cap(2);
// if two whitespaces in row - that's an empty cell
if (!(pos - whitespace_offset)) {
result.back().push_back(QByteArray());
} else {
text = clipboard.mid(whitespace_offset, pos - whitespace_offset);
if(QRegExp("\".*\"").exactMatch(text))
text = text.mid(1, text.length() - 2);
text.replace("\"\"", "\"");
result.back().push_back(text.toUtf8());
}
if (ws.endsWith("\n"))
// create new row
result.push_back(BufferRow());
whitespace_offset = offset = pos + ws.length();
}
return result;
}
}
UniqueFilterModel::UniqueFilterModel(QObject* parent)
: QSortFilterProxyModel(parent)
{
}
bool UniqueFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent);
const std::string value = index.data(Qt::EditRole).toString().toStdString();
if (!value.empty() && m_uniqueValues.find(value) == m_uniqueValues.end()) {
const_cast<UniqueFilterModel*>(this)->m_uniqueValues.insert(value);
return true;
}
else
return false;
}
ExtendedTableWidgetEditorDelegate::ExtendedTableWidgetEditorDelegate(QObject* parent)
: QStyledItemDelegate(parent)
{
}
QWidget* ExtendedTableWidgetEditorDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) const
{
emit dataAboutToBeEdited(index);
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(const_cast<QAbstractItemModel*>(index.model()));
sqlb::ForeignKeyClause fk = m->getForeignKeyClause(static_cast<size_t>(index.column()-1));
if(fk.isSet()) {
sqlb::ObjectIdentifier foreignTable = sqlb::ObjectIdentifier(m->currentTableName().schema(), fk.table());
std::string column;
// If no column name is set, assume the primary key is meant
if(fk.columns().empty()) {
sqlb::TablePtr obj = m->db().getTableByName(foreignTable);
column = obj->primaryKey()->columnList().front();
} else
column = fk.columns().at(0);
sqlb::TablePtr currentTable = m->db().getTableByName(m->currentTableName());
QString query = QString("SELECT %1 FROM %2").arg(QString::fromStdString(sqlb::escapeIdentifier(column)), QString::fromStdString(foreignTable.toString()));
// if the current column of the current table does NOT have not-null constraint,
// the NULL is united to the query to get the possible values in the combo-box.
if (!currentTable->fields.at(static_cast<size_t>(index.column())-1).notnull())
query.append (" UNION SELECT NULL");
SqliteTableModel* fkModel = new SqliteTableModel(m->db(), parent, m->encoding());
fkModel->setQuery(query);
QComboBox* combo = new QComboBox(parent);
// Complete cache so it is ready when setEditorData is invoked.
fkModel->completeCache();
combo->setModel(fkModel);
return combo;
} else {
QLineEdit* editor = new QLineEdit(parent);
// If the row count is not greater than the complete threshold setting, set a completer of values based on current values in the column.
if (index.model()->rowCount() <= Settings::getValue("databrowser", "complete_threshold").toInt()) {
QCompleter* completer = new QCompleter(editor);
UniqueFilterModel* completerFilter = new UniqueFilterModel(completer);
// Provide a filter for the source model, so only unique and non-empty values are accepted.
completerFilter->setSourceModel(const_cast<QAbstractItemModel*>(index.model()));
completerFilter->setFilterKeyColumn(index.column());
completer->setModel(completerFilter);
// Complete on this column, using a popup and case-insensitively.
completer->setCompletionColumn(index.column());
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setCaseSensitivity(Qt::CaseInsensitive);
editor->setCompleter(completer);
}
// Set the maximum length to the highest possible value instead of the default 32768.
editor->setMaxLength(std::numeric_limits<int>::max());
return editor;
}
}
void ExtendedTableWidgetEditorDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
QLineEdit* lineedit = dynamic_cast<QLineEdit*>(editor);
// Set the data for the editor
QString data = index.data(Qt::EditRole).toString();
if(!lineedit) {
QComboBox* combo = static_cast<QComboBox*>(editor);
int comboIndex = combo->findText(data);
if (comboIndex >= 0)
// if it is valid, adjust the combobox
combo->setCurrentIndex(comboIndex);
} else {
lineedit->setText(data);
// Put the editor in read only mode if the actual data is larger than the maximum length to avoid accidental truncation of the data
lineedit->setReadOnly(data.size() > lineedit->maxLength());
}
}
void ExtendedTableWidgetEditorDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
{
// Only apply the data back to the model if the editor is not in read only mode to avoid accidental truncation of the data
QLineEdit* lineedit = dynamic_cast<QLineEdit*>(editor);
if(!lineedit) {
QComboBox* combo = static_cast<QComboBox*>(editor);
model->setData(index, combo->currentData(Qt::EditRole), Qt::EditRole);
} else
if(!lineedit->isReadOnly())
model->setData(index, lineedit->text());
}
void ExtendedTableWidgetEditorDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& /*index*/) const
{
editor->setGeometry(option.rect);
}
class ItemBorderDelegate : public QStyledItemDelegate
{
public:
explicit ItemBorderDelegate(QObject* parent = nullptr)
: QStyledItemDelegate(parent)
{
}
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
{
QStyledItemDelegate::paint(painter, option, index);
QRect border(option.rect);
border.translate(border.width() - 1, 0);
border.setWidth(2);
painter->fillRect(border, Qt::black);
}
};
ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) :
QTableView(parent),
m_frozen_table_view(qobject_cast<ExtendedTableWidget*>(parent) ? nullptr : new ExtendedTableWidget(this)),
m_frozen_column_count(0),
m_item_border_delegate(new ItemBorderDelegate(this))
{
setHorizontalScrollMode(ExtendedTableWidget::ScrollPerPixel);
// Force ScrollPerItem, so scrolling shows all table rows
setVerticalScrollMode(ExtendedTableWidget::ScrollPerItem);
connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &ExtendedTableWidget::vscrollbarChanged);
connect(this, &ExtendedTableWidget::clicked, this, &ExtendedTableWidget::cellClicked);
// Set up filter row
m_tableHeader = new FilterTableHeader(this);
setHorizontalHeader(m_tableHeader);
// Disconnect clicking in header to select column, since we will use it for sorting.
// Note that, in order to work, this cannot be converted to the standard C++11 format.
disconnect(m_tableHeader, SIGNAL(sectionPressed(int)),this, SLOT(selectColumn(int)));
// Set up vertical header context menu
verticalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
// Set up table view context menu
m_contextMenu = new QMenu(this);
QAction* filterAction = new QAction(QIcon(":/icons/filter"), tr("Use as Exact Filter"), m_contextMenu);
QAction* containingAction = new QAction(tr("Containing"), m_contextMenu);
QAction* notContainingAction = new QAction(tr("Not containing"), m_contextMenu);
QAction* notEqualToAction = new QAction(tr("Not equal to"), m_contextMenu);
QAction* greaterThanAction = new QAction(tr("Greater than"), m_contextMenu);
QAction* lessThanAction = new QAction(tr("Less than"), m_contextMenu);
QAction* greaterEqualAction = new QAction(tr("Greater or equal"), m_contextMenu);
QAction* lessEqualAction = new QAction(tr("Less or equal"), m_contextMenu);
QAction* inRangeAction = new QAction(tr("Between this and..."), m_contextMenu);
QAction* regexpAction = new QAction(tr("Regular expression"), m_contextMenu);
QAction* condFormatAction = new QAction(QIcon(":/icons/edit_cond_formats"), tr("Edit Conditional Formats..."), m_contextMenu);
QAction* nullAction = new QAction(QIcon(":/icons/set_to_null"), tr("Set to NULL"), m_contextMenu);
QAction* cutAction = new QAction(QIcon(":/icons/cut"), tr("Cut"), m_contextMenu);
QAction* copyAction = new QAction(QIcon(":/icons/copy"), tr("Copy"), m_contextMenu);
QAction* copyWithHeadersAction = new QAction(QIcon(":/icons/special_copy"), tr("Copy with Headers"), m_contextMenu);
QAction* copyAsSQLAction = new QAction(QIcon(":/icons/sql_copy"), tr("Copy as SQL"), m_contextMenu);
QAction* pasteAction = new QAction(QIcon(":/icons/paste"), tr("Paste"), m_contextMenu);
QAction* printAction = new QAction(QIcon(":/icons/print"), tr("Print..."), m_contextMenu);
m_contextMenu->addAction(filterAction);
QMenu* filterMenu = m_contextMenu->addMenu(tr("Use in Filter Expression"));
filterMenu->addAction(containingAction);
filterMenu->addAction(notContainingAction);
filterMenu->addAction(notEqualToAction);
filterMenu->addAction(greaterThanAction);
filterMenu->addAction(lessThanAction);
filterMenu->addAction(greaterEqualAction);
filterMenu->addAction(lessEqualAction);
filterMenu->addAction(inRangeAction);
filterMenu->addAction(regexpAction);
m_contextMenu->addAction(condFormatAction);
m_contextMenu->addSeparator();
m_contextMenu->addAction(nullAction);
m_contextMenu->addSeparator();
m_contextMenu->addAction(cutAction);
m_contextMenu->addAction(copyAction);
m_contextMenu->addAction(copyWithHeadersAction);
m_contextMenu->addAction(copyAsSQLAction);
m_contextMenu->addAction(pasteAction);
m_contextMenu->addSeparator();
m_contextMenu->addAction(printAction);
setContextMenuPolicy(Qt::CustomContextMenu);
// Create and set up delegate
m_editorDelegate = new ExtendedTableWidgetEditorDelegate(this);
setItemDelegate(m_editorDelegate);
connect(m_editorDelegate, &ExtendedTableWidgetEditorDelegate::dataAboutToBeEdited, this, &ExtendedTableWidget::dataAboutToBeEdited);
// This is only for displaying the shortcut in the context menu.
// An entry in keyPressEvent is still needed.
nullAction->setShortcut(QKeySequence(tr("Alt+Del")));
cutAction->setShortcut(QKeySequence::Cut);
copyAction->setShortcut(QKeySequence::Copy);
copyWithHeadersAction->setShortcut(QKeySequence(tr("Ctrl+Shift+C")));
copyAsSQLAction->setShortcut(QKeySequence(tr("Ctrl+Alt+C")));
pasteAction->setShortcut(QKeySequence::Paste);
printAction->setShortcut(QKeySequence::Print);
// Set up context menu actions
connect(this, &QTableView::customContextMenuRequested, this,
[=](const QPoint& pos)
{
// Deactivate context menu options if there is no model set
bool enabled = model();
filterAction->setEnabled(enabled);
filterMenu->setEnabled(enabled);
copyAction->setEnabled(enabled);
copyWithHeadersAction->setEnabled(enabled);
copyAsSQLAction->setEnabled(enabled);
printAction->setEnabled(enabled);
condFormatAction->setEnabled(enabled);
// Hide filter actions when there isn't any filters
bool hasFilters = m_tableHeader->hasFilters();
filterAction->setVisible(hasFilters);
filterMenu->menuAction()->setVisible(hasFilters);
condFormatAction->setVisible(hasFilters);
// Try to find out whether the current view is editable and (de)activate menu options according to that
bool editable = editTriggers() != QAbstractItemView::NoEditTriggers;
nullAction->setEnabled(enabled && editable);
cutAction->setEnabled(enabled && editable);
pasteAction->setEnabled(enabled && editable);
// Show menu
m_contextMenu->popup(viewport()->mapToGlobal(pos));
});
connect(filterAction, &QAction::triggered, this, [&]() {
useAsFilter(QString ("="));
});
connect(containingAction, &QAction::triggered, this, [&]() {
useAsFilter(QString (""));
});
// Use a regular expression for the not containing filter. Simplify this if we ever support the NOT LIKE filter.
connect(notContainingAction, &QAction::triggered, this, [&]() {
useAsFilter(QString ("/^((?!"), /* binary */ false, QString (").)*$/"));
});
connect(notEqualToAction, &QAction::triggered, this, [&]() {
useAsFilter(QString ("<>"));
});
connect(greaterThanAction, &QAction::triggered, this, [&]() {
useAsFilter(QString (">"));
});
connect(lessThanAction, &QAction::triggered, this, [&]() {
useAsFilter(QString ("<"));
});
connect(greaterEqualAction, &QAction::triggered, this, [&]() {
useAsFilter(QString (">="));
});
connect(lessEqualAction, &QAction::triggered, this, [&]() {
useAsFilter(QString ("<="));
});
connect(inRangeAction, &QAction::triggered, this, [&]() {
useAsFilter(QString ("~"), /* binary */ true);
});
connect(regexpAction, &QAction::triggered, this, [&]() {
useAsFilter(QString ("/"), /* binary */ false, QString ("/"));
});
connect(condFormatAction, &QAction::triggered, this, [&]() {
emit editCondFormats(currentIndex().column());
});
connect(nullAction, &QAction::triggered, this, [&]() {
setToNull(selectedIndexes());
});
connect(copyAction, &QAction::triggered, this, [&]() {
copy(false, false);
});
connect(cutAction, &QAction::triggered, this, &ExtendedTableWidget::cut);
connect(copyWithHeadersAction, &QAction::triggered, this, [&]() {
copy(true, false);
});
connect(copyAsSQLAction, &QAction::triggered, this, [&]() {
copy(false, true);
});
connect(pasteAction, &QAction::triggered, this, [&]() {
paste();
});
connect(printAction, &QAction::triggered, this, [&]() {
openPrintDialog();
});
// Add spreadsheet shortcuts for selecting entire columns or entire rows.
QShortcut* selectColumnShortcut = new QShortcut(QKeySequence("Ctrl+Space"), this);
connect(selectColumnShortcut, &QShortcut::activated, this, [this]() {
if(!hasFocus() || selectionModel()->selectedIndexes().isEmpty())
return;
selectionModel()->select(selectionModel()->selection(), QItemSelectionModel::Select | QItemSelectionModel::Columns);
});
QShortcut* selectRowShortcut = new QShortcut(QKeySequence("Shift+Space"), this);
connect(selectRowShortcut, &QShortcut::activated, this, [this]() {
if(!hasFocus() || selectionModel()->selectedIndexes().isEmpty())
return;
selectionModel()->select(selectionModel()->selection(), QItemSelectionModel::Select | QItemSelectionModel::Rows);
});
// Set up frozen columns child widget
if(m_frozen_table_view)
{
// Set up widget
m_frozen_table_view->setFocusPolicy(Qt::NoFocus);
m_frozen_table_view->verticalHeader()->hide();
m_frozen_table_view->setStyleSheet("QTableView { border: none; }"
"QTableView::item:selected{ background-color: " + palette().color(QPalette::Active, QPalette::Highlight).name() + "}");
m_frozen_table_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_frozen_table_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_frozen_table_view->setVerticalScrollMode(verticalScrollMode());
viewport()->stackUnder(m_frozen_table_view);
m_tableHeader->stackUnder(m_frozen_table_view);
// Keep both widgets in sync
connect(horizontalHeader(), &QHeaderView::sectionResized, this, &ExtendedTableWidget::updateSectionWidth);
connect(m_frozen_table_view->horizontalHeader(), &QHeaderView::sectionResized, this, &ExtendedTableWidget::updateSectionWidth);
connect(verticalHeader(), &QHeaderView::sectionResized, this, &ExtendedTableWidget::updateSectionHeight);
connect(m_frozen_table_view->verticalHeader(), &QHeaderView::sectionResized, this, &ExtendedTableWidget::updateSectionHeight);
connect(m_frozen_table_view->verticalScrollBar(), &QAbstractSlider::valueChanged, verticalScrollBar(), &QAbstractSlider::setValue);
connect(verticalScrollBar(), &QAbstractSlider::valueChanged, m_frozen_table_view->verticalScrollBar(), &QAbstractSlider::setValue);
// Forward signals from frozen table view widget to the main table view widget
connect(m_frozen_table_view, &ExtendedTableWidget::doubleClicked, this, &ExtendedTableWidget::doubleClicked);
connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::sectionClicked, filterHeader(), &FilterTableHeader::sectionClicked);
connect(m_frozen_table_view->filterHeader(), &QHeaderView::sectionDoubleClicked, filterHeader(), &QHeaderView::sectionDoubleClicked);
connect(m_frozen_table_view->verticalHeader(), &QHeaderView::sectionResized, verticalHeader(), &QHeaderView::sectionResized);
connect(m_frozen_table_view->horizontalHeader(), &QHeaderView::customContextMenuRequested, horizontalHeader(), &QHeaderView::customContextMenuRequested);
connect(m_frozen_table_view->verticalHeader(), &QHeaderView::customContextMenuRequested, verticalHeader(), &QHeaderView::customContextMenuRequested);
connect(m_frozen_table_view, &ExtendedTableWidget::openFileFromDropEvent, this, &ExtendedTableWidget::openFileFromDropEvent);
connect(m_frozen_table_view, &ExtendedTableWidget::selectedRowsToBeDeleted, this, &ExtendedTableWidget::selectedRowsToBeDeleted);
connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::filterChanged, filterHeader(), &FilterTableHeader::filterChanged);
connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::addCondFormat, filterHeader(), &FilterTableHeader::addCondFormat);
connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::allCondFormatsCleared, filterHeader(), &FilterTableHeader::allCondFormatsCleared);
connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::condFormatsEdited, filterHeader(), &FilterTableHeader::condFormatsEdited);
connect(m_frozen_table_view, &ExtendedTableWidget::editCondFormats, this, &ExtendedTableWidget::editCondFormats);
connect(m_frozen_table_view, &ExtendedTableWidget::dataAboutToBeEdited, this, &ExtendedTableWidget::dataAboutToBeEdited);
connect(m_frozen_table_view, &ExtendedTableWidget::foreignKeyClicked, this, &ExtendedTableWidget::foreignKeyClicked);
connect(m_frozen_table_view, &ExtendedTableWidget::currentIndexChanged, this, &ExtendedTableWidget::currentIndexChanged);
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) && QT_VERSION < QT_VERSION_CHECK(5, 12, 3)
// This work arounds QTBUG-73721 and it is applied only for the affected version range.
setWordWrap(false);
#endif
}
ExtendedTableWidget::~ExtendedTableWidget()
{
delete m_frozen_table_view;
}
void ExtendedTableWidget::setModel(QAbstractItemModel* item_model)
{
// Set model
QTableView::setModel(item_model);
// Set up frozen table view widget
if(item_model)
setFrozenColumns(m_frozen_column_count);
else
m_frozen_table_view->hide();
}
void ExtendedTableWidget::reloadSettings()
{
// We only get the font here to get its metrics. The actual font for the view is set in the model
QFont dataBrowserFont(Settings::getValue("databrowser", "font").toString());
dataBrowserFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt());
// Set new default row height depending on the font size
QFontMetrics fontMetrics(dataBrowserFont);
verticalHeader()->setDefaultSectionSize(fontMetrics.height()+10);
if(m_frozen_table_view)
m_frozen_table_view->reloadSettings();
}
void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMimeData* mimeData, const bool withHeaders, const bool inSQL)
{
QModelIndexList indices = fromIndices;
// Remove all indices from hidden columns, because if we don't we might copy data from hidden columns as well which is very
// unintuitive; especially copying the rowid column when selecting all columns of a table is a problem because pasting the data
// won't work as expected.
QMutableListIterator<QModelIndex> it(indices);
while (it.hasNext()) {
if (isColumnHidden(it.next().column()))
it.remove();
}
// Abort if there's nothing to copy
if (indices.isEmpty())
return;
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
// Clear internal copy-paste buffer
m_buffer.clear();
// If a single cell is selected which contains an image, copy it to the clipboard
if (!inSQL && !withHeaders && indices.size() == 1) {
QImage img;
QVariant varData = m->data(indices.first(), Qt::EditRole);
if (img.loadFromData(varData.toByteArray()))
{
// If it's an image, copy the image data to the clipboard
mimeData->setImageData(img);
return;
}
}
// If we got here, a non-image cell was or multiple cells were selected, or copy with headers was requested.
// In this case, we copy selected data into internal copy-paste buffer and then
// we write a table both in HTML and text formats to the system clipboard.
// Copy selected data into internal copy-paste buffer
int last_row = indices.first().row();
BufferRow lst;
for(int i=0;i<indices.size();i++)
{
if(indices.at(i).row() != last_row)
{
m_buffer.push_back(lst);
lst.clear();
}
lst.push_back(indices.at(i).data(Qt::EditRole).toByteArray());
last_row = indices.at(i).row();
}
m_buffer.push_back(lst);
QString sqlResult;
QString result;
QString htmlResult = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">";
htmlResult.append("<html><head><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">");
htmlResult.append("<title></title>");
// The generator-stamp is later used to know whether the data in the system clipboard is still ours.
// In that case we will give precedence to our internal copy buffer.
QString now = QDateTime::currentDateTime().toString("YYYY-MM-DDTHH:mm:ss.zzz");
m_generatorStamp = QString("<meta name=\"generator\" content=\"%1\"><meta name=\"date\" content=\"%2\">").arg(QApplication::applicationName().toHtmlEscaped(), now);
htmlResult.append(m_generatorStamp);
// TODO: is this really needed by Excel, since we use <pre> for multi-line cells?
htmlResult.append("<style type=\"text/css\">br{mso-data-placement:same-cell;}</style></head><body>"
"<table border=1 cellspacing=0 cellpadding=2>");
// Insert the columns in a set, since they could be non-contiguous.
std::set<int> colsInIndexes, rowsInIndexes;
for(const QModelIndex & idx : indices) {
colsInIndexes.insert(idx.column());
rowsInIndexes.insert(idx.row());
}
int currentRow = indices.first().row();
const QString fieldSepText = "\t";
#ifdef Q_OS_WIN
const QString rowSepText = "\r\n";
#else
const QString rowSepText = "\n";
#endif
QString sqlInsertStatement = QString("INSERT INTO %1 (").arg(QString::fromStdString(m->currentTableName().toString()));
// Table headers
if (withHeaders || inSQL) {
htmlResult.append("<tr><th>");
int firstColumn = *colsInIndexes.begin();
for(int col : colsInIndexes) {
QByteArray headerText = model()->headerData(col, Qt::Horizontal, Qt::EditRole).toByteArray();
if (col != firstColumn) {
result.append(fieldSepText);
htmlResult.append("</th><th>");
sqlInsertStatement.append(", ");
}
result.append(headerText);
htmlResult.append(headerText);
sqlInsertStatement.append(sqlb::escapeIdentifier(headerText));
}
result.append(rowSepText);
htmlResult.append("</th></tr>");
sqlInsertStatement.append(") VALUES (");
}
// Iterate over rows x cols checking if the index actually exists when needed, in order
// to support non-rectangular selections.
for(const int row : rowsInIndexes) {
for(const int column : colsInIndexes) {
const QModelIndex index = indices.first().sibling(row, column);
QString style;
if(indices.contains(index)) {
QFont font;
font.fromString(index.data(Qt::FontRole).toString());
const QString fontStyle(font.italic() ? "italic" : "normal");
const QString fontWeigth(font.bold() ? "bold" : "normal");
const QString fontDecoration(font.underline() ? " text-decoration: underline;" : "");
const QColor bgColor(index.data(Qt::BackgroundRole).toString());
const QColor fgColor(index.data(Qt::ForegroundRole).toString());
const Qt::Alignment align(index.data(Qt::TextAlignmentRole).toInt());
const QString textAlign(CondFormat::alignmentTexts().at(CondFormat::fromCombinedAlignment(align)).toLower());
style = QString("style=\"font-family: '%1'; font-size: %2pt; font-style: %3; font-weight: %4;%5 "
"background-color: %6; color: %7; text-align: %8\"").arg(
font.family().toHtmlEscaped(),
QString::number(font.pointSize()),
fontStyle,
fontWeigth,
fontDecoration,
bgColor.name(),
fgColor.name(),
textAlign);
}
// Separators. For first cell, only opening table row tags must be added for the HTML and nothing for the text version.
if (index.row() == *rowsInIndexes.begin() && index.column() == *colsInIndexes.begin()) {
htmlResult.append(QString("<tr><td %1>").arg(style));
sqlResult.append(sqlInsertStatement);
} else if (index.row() != currentRow) {
result.append(rowSepText);
htmlResult.append(QString("</td></tr><tr><td %1>").arg(style));
sqlResult.append(");" + rowSepText + sqlInsertStatement);
} else {
result.append(fieldSepText);
htmlResult.append(QString("</td><td %1>").arg(style));
sqlResult.append(", ");
}
currentRow = index.row();
QImage img;
QVariant bArrdata = indices.contains(index) ? index.data(Qt::EditRole) : QVariant();
// Table cell data: image? Store it as an embedded image in HTML
if (!inSQL && img.loadFromData(bArrdata.toByteArray()))
{
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
img.save(&buffer, "PNG");
buffer.close();
QString imageBase64 = ba.toBase64();
htmlResult.append("<img src=\"data:image/png;base64,");
htmlResult.append(imageBase64);
result.append(QString());
htmlResult.append("\" alt=\"Image\">");
} else {
if (bArrdata.isNull()) {
sqlResult.append("NULL");
} else if(!m->isBinary(index)) {
QByteArray text = bArrdata.toByteArray();
// Table cell data: text
if (text.contains('\n') || text.contains('\t'))
htmlResult.append("<pre>" + QString(text).toHtmlEscaped() + "</pre>");
else
htmlResult.append(QString(text).toHtmlEscaped());
result.append(text);
sqlResult.append(sqlb::escapeString(text));
} else
// Table cell data: binary. Save as BLOB literal in SQL
sqlResult.append( "X'" + bArrdata.toByteArray().toHex() + "'" );
}
}
}
sqlResult.append(");");
if ( inSQL )
{
mimeData->setText(sqlResult);
} else {
mimeData->setHtml(htmlResult + "</td></tr></table></body></html>");
mimeData->setText(result);
}
}
void ExtendedTableWidget::copy(const bool withHeaders, const bool inSQL )
{
QMimeData *mimeData = new QMimeData;
copyMimeData(selectionModel()->selectedIndexes(), mimeData, withHeaders, inSQL);
qApp->clipboard()->setMimeData(mimeData);
}
void ExtendedTableWidget::paste()
{
// Get list of selected items
QItemSelectionModel* selection = selectionModel();
QModelIndexList indices = selection->selectedIndexes();
// Abort if there's nowhere to paste
if(indices.isEmpty())
return;
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
// We're also checking for system clipboard data first. Only if the data in the system clipboard is not ours, we use the system
// clipboard, otherwise we prefer the internal buffer. That's because the data in the internal buffer is easier to parse and more
// accurate, too. However, if we always preferred the internal copy-paste buffer there would be no way to copy data from other
// applications in here once the internal buffer has been filled.
// If clipboard contains an image and no text, just insert the image
const QMimeData* mimeClipboard = qApp->clipboard()->mimeData();
if (mimeClipboard->hasImage() && !mimeClipboard->hasText()) {
QImage img = qApp->clipboard()->image();
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
img.save(&buffer, "PNG"); // We're always converting the image format to PNG here. TODO: Is that correct?
buffer.close();
m->setData(indices.first(), ba);
return;
}
// Get the clipboard text
QString clipboard = qApp->clipboard()->text();
// If data in system clipboard is ours and the internal copy-paste buffer is filled, use the internal buffer; otherwise parse the
// system clipboard contents (case for data copied by other application).
std::vector<BufferRow> clipboardTable;
std::vector<BufferRow>* source;
if(mimeClipboard->hasHtml() && mimeClipboard->html().contains(m_generatorStamp) && !m_buffer.empty())
{
source = &m_buffer;
} else {
clipboardTable = parseClipboard(clipboard);
source = &clipboardTable;
}
// Stop here if there's nothing to paste
if(!source->size())
return;
// Starting from assumption that selection is rectangular, and then first index is upper-left corner and last is lower-right.
int rows = static_cast<int>(source->size());
int columns = static_cast<int>(source->front().size());
int firstRow = indices.front().row();
int firstColumn = indices.front().column();
int selectedRows = indices.back().row() - firstRow + 1;
int selectedColumns = indices.back().column() - firstColumn + 1;
// If last row and column are after table size, clamp it
int lastRow = qMin(firstRow + rows - 1, m->rowCount() - 1);
int lastColumn = qMin(firstColumn + columns - 1, m->columnCount() - 1);
// Special case: if there is only one cell of data to be pasted, paste it into all selected fields
if(rows == 1 && columns == 1)
{
QByteArray bArrdata = source->front().front();
for(int row=firstRow;row<firstRow+selectedRows;row++)
{
for(int column=firstColumn;column<firstColumn+selectedColumns;column++)
m->setData(m->index(row, column), bArrdata);
}
return;
}
// If more than one cell was selected, check if the selection matches the cliboard dimensions
if(selectedRows != rows || selectedColumns != columns)
{
// Ask user if they are sure about this
if(QMessageBox::question(this, QApplication::applicationName(),
tr("The content of the clipboard is bigger than the range selected.\nDo you want to insert it anyway?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
{
// If the user doesn't want to paste the clipboard data anymore, stop now
return;
}
}
// If we get here, we can definitely start pasting: either the ranges match in their size or the user agreed to paste anyway
// Copy the data cell by cell and as-is from the source buffer to the table
int row = firstRow;
for(const auto& source_row : *source)
{
int column = firstColumn;
for(const QByteArray& source_cell : source_row)
{
m->setData(m->index(row, column), source_cell);
column++;
if (column > lastColumn)
break;
}
row++;
if (row > lastRow)
break;
}
}
void ExtendedTableWidget::cut()
{
const QModelIndexList& indices = selectionModel()->selectedIndexes();
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
sqlb::TablePtr currentTable = m->db().getTableByName(m->currentTableName());
copy(false, false);
// Check if the column in the selection has a NOT NULL constraint, then update with an empty string, else with NULL
if(currentTable) {
for(const QModelIndex& index : indices) {
// Do not process rowid column
if(index.column() != 0) {
const size_t indexField = static_cast<size_t>(index.column()-1);
const sqlb::Field& field = currentTable->fields.at(indexField);
const QVariant newValue = field.notnull() ? QVariant("") : QVariant();
// Update aborting in case of any error (to avoid repetitive errors like "Database is locked")
if(!model()->setData(index, newValue))
return;
}
}
}
}
void ExtendedTableWidget::useAsFilter(const QString& filterOperator, bool binary, const QString& operatorSuffix)
{
QModelIndex index = selectionModel()->currentIndex();
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
// Abort if there's nothing to filter
if (!index.isValid() || !selectionModel()->hasSelection() || m->isBinary(index))
return;
QVariant bArrdata = model()->data(index, Qt::EditRole);
QString value;
if (bArrdata.isNull())
value = "NULL";
else if (bArrdata.toString().isEmpty())
value = "''";
else
value = bArrdata.toString();
// When Containing filter is requested (empty operator) and the value starts with
// an operator character, the character is escaped.
if (filterOperator.isEmpty())
value.replace(QRegExp("^(<|>|=|/)"), Settings::getValue("databrowser", "filter_escape").toString() + QString("\\1"));
// If binary operator, the cell data is used as first value and
// the second value must be added by the user.
size_t column = static_cast<size_t>(index.column());
if (binary)
m_tableHeader->setFilter(column, value + filterOperator);
else
m_tableHeader->setFilter(column, filterOperator + value + operatorSuffix);
}
void ExtendedTableWidget::duplicateUpperCell()
{
const QModelIndex& currentIndex = selectionModel()->currentIndex();
QModelIndex upperIndex = currentIndex.sibling(currentIndex.row() - 1, currentIndex.column());
if (upperIndex.isValid()) {
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
// When the data is binary, just copy it, since it cannot be edited inline.
if (m->isBinary(upperIndex))
m->setData(currentIndex, m->data(upperIndex, Qt::EditRole), Qt::EditRole);
else {
// Open the inline editor and set the value (this mimics the behaviour of LibreOffice Calc)
edit(currentIndex);
QLineEdit* editor = qobject_cast<QLineEdit*>(indexWidget(currentIndex));
editor->setText(upperIndex.data().toString());
}
}
}
void ExtendedTableWidget::keyPressEvent(QKeyEvent* event)
{
// Call a custom copy method when Ctrl-C is pressed
if(event->matches(QKeySequence::Copy))
{
copy(false, false);
return;
} else if(event->matches(QKeySequence::Cut)) {
// Call a custom cut method when Ctrl-X is pressed
cut();
} else if(event->matches(QKeySequence::Paste)) {
// Call a custom paste method when Ctrl-V is pressed
paste();
} else if(event->matches(QKeySequence::Print)) {
openPrintDialog();
} else if(event->modifiers().testFlag(Qt::ControlModifier) && event->modifiers().testFlag(Qt::ShiftModifier) && (event->key() == Qt::Key_C)) {
// Call copy with headers when Ctrl-Shift-C is pressed
copy(true, false);
} else if(event->modifiers().testFlag(Qt::ControlModifier) && event->modifiers().testFlag(Qt::AltModifier) && (event->key() == Qt::Key_C)) {
// Call copy in SQL format when Ctrl-Alt-C is pressed
copy(false, true);
} else if(event->modifiers().testFlag(Qt::ControlModifier) && (event->key() == Qt::Key_Apostrophe)) {
// Call duplicateUpperCell when Ctrl-' is pressed (this is used by spreadsheets for "Copy Formula from Cell Above")
duplicateUpperCell();
} else if(event->key() == Qt::Key_Tab && hasFocus() &&
selectedIndexes().count() == 1 &&
selectedIndexes().at(0).row() == model()->rowCount()-1 && selectedIndexes().at(0).column() == model()->columnCount()-1) {
// If the Tab key was pressed while the focus was on the last cell of the last row insert a new row automatically
model()->insertRow(model()->rowCount());
} else if ((event->key() == Qt::Key_Delete) || (event->key() == Qt::Key_Backspace)) {
// Check if entire rows are selected. We call the selectedRows() method here not only for simplicity reasons but also because it distinguishes between
// "an entire row is selected" and "all cells of a row are selected", the former is e.g. the case when the row number is clicked, the latter when all cells
// are selected manually. This is an important distinction (especially when a table has only one column!) to match the users' expectations. Also never
// delete records when the backspace key was pressed.
if(event->key() == Qt::Key_Delete && selectionModel()->selectedRows().size())
{
// At least on entire row is selected. Because we don't allow completely arbitrary selections (at least at the moment) but only block selections,
// this means that only entire entire rows are selected. If an entire row is (or multiple entire rows are) selected, we delete that record instead
// of deleting only the cell contents.
emit selectedRowsToBeDeleted();
} else {
// No entire row is selected. So just set the selected cells to null or empty string depending on the modifier keys
if(event->modifiers().testFlag(Qt::AltModifier))
{
// When pressing Alt+Delete set the value to NULL
setToNull(selectedIndexes());
} else {
// When pressing Delete only set the value to empty string
for(const QModelIndex& index : selectedIndexes())
{
if(!model()->setData(index, ""))
return;
}
}
}
} else if(event->modifiers().testFlag(Qt::ControlModifier) && (event->key() == Qt::Key_PageUp || event->key() == Qt::Key_PageDown)) {
// When pressing Ctrl + Page up/down send a signal indicating the user wants to change the current table
emit switchTable(event->key() == Qt::Key_PageDown);
return;
}
// This prevents the current selection from being changed when pressing tab to move to the next filter. Note that this is in an 'if' condition,
// not in an 'else if' because this way, when the tab key was pressed and the focus was on the last cell, a new row is inserted and then the tab
// key press is processed a second time to move the cursor as well
if((event->key() != Qt::Key_Tab && event->key() != Qt::Key_Backtab) || hasFocus())
QTableView::keyPressEvent(event);
}
void ExtendedTableWidget::updateGeometries()
{
// Call the parent implementation first - it does most of the actual logic
QTableView::updateGeometries();
// Update frozen columns view too
if(m_frozen_table_view)
m_frozen_table_view->updateGeometries();
// Check if a model has already been set yet
if(model())
{
// If so and if it is a SqliteTableModel and if the parent implementation of this method decided that a scrollbar is needed, update its maximum value
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
if(m && verticalScrollBar()->maximum())
{
verticalScrollBar()->setMaximum(m->rowCount() - numVisibleRows() + 1);
if(m_frozen_table_view)
m_frozen_table_view->verticalScrollBar()->setMaximum(verticalScrollBar()->maximum());
}
}
}
void ExtendedTableWidget::vscrollbarChanged(int value)
{
// Cancel if there is no model set yet - this shouldn't happen (because without a model there should be no scrollbar) but just to be sure...
if(!model())
return;
// Fetch more data from the DB if necessary
const auto nrows = model()->rowCount();
if(nrows == 0)
return;
if(auto * m = dynamic_cast<SqliteTableModel*>(model()))
{
int row_begin = std::min(value, nrows - 1);
int row_end = std::min(value + numVisibleRows(), nrows);
m->triggerCacheLoad(row_begin, row_end);
}
}
int ExtendedTableWidget::numVisibleRows() const
{
if(!isVisible() || !model() || !verticalHeader())
return 0;
// Get the row numbers of the rows currently visible at the top and the bottom of the widget
int row_top = rowAt(0) == -1 ? 0 : rowAt(0);
int row_bottom = verticalHeader()->visualIndexAt(height()) == -1 ? model()->rowCount() : (verticalHeader()->visualIndexAt(height()) - 1);
if(horizontalScrollBar()->isVisible()) // Assume the scrollbar covers about one row
row_bottom--;
// Calculate the number of visible rows
return row_bottom - row_top;
}
std::unordered_set<size_t> ExtendedTableWidget::selectedCols() const
{
std::unordered_set<size_t> selectedCols;
for(const auto& idx : selectionModel()->selectedColumns())
selectedCols.insert(static_cast<size_t>(idx.column()));
return selectedCols;
}
std::unordered_set<size_t> ExtendedTableWidget::colsInSelection() const
{
std::unordered_set<size_t> colsInSelection;
for(const QModelIndex & idx : selectedIndexes())
colsInSelection.insert(static_cast<size_t>(idx.column()));
return colsInSelection;
}
std::set<size_t> ExtendedTableWidget::rowsInSelection() const
{
std::set<size_t> rowsInSelection;
for(const QModelIndex & idx : selectedIndexes())
rowsInSelection.insert(static_cast<size_t>(idx.row()));
return rowsInSelection;
}
void ExtendedTableWidget::cellClicked(const QModelIndex& index)
{
// If Ctrl-Shift is pressed try to jump to the row referenced by the foreign key of the clicked cell
if(qApp->keyboardModifiers().testFlag(Qt::ControlModifier) && qApp->keyboardModifiers().testFlag(Qt::ShiftModifier) && model())
{
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
sqlb::ForeignKeyClause fk = m->getForeignKeyClause(static_cast<size_t>(index.column()-1));
if(fk.isSet())
emit foreignKeyClicked(sqlb::ObjectIdentifier(m->currentTableName().schema(), fk.table()),
fk.columns().size() ? fk.columns().at(0) : "",
m->data(index, Qt::EditRole).toByteArray());
else {
// If this column does not have a foreign-key, try to interpret it as a filename/URL and open it in external application.
// TODO: Qt is doing a contiguous selection when Control+Click is pressed. It should be disabled, but at least moving the
// current index gives better result.
setCurrentIndex(index);
emit requestUrlOrFileOpen(model()->data(index, Qt::EditRole).toString());
}
}
}
void ExtendedTableWidget::dragEnterEvent(QDragEnterEvent* event)
{
event->accept();
}
void ExtendedTableWidget::dragMoveEvent(QDragMoveEvent* event)
{
event->accept();
}
void ExtendedTableWidget::dropEvent(QDropEvent* event)
{
QModelIndex index = indexAt(event->pos());
if (!index.isValid())
{
if (event->mimeData()->hasUrls() && event->mimeData()->urls().first().isLocalFile())
emit openFileFromDropEvent(event->mimeData()->urls().first().toLocalFile());
return;
}
model()->dropMimeData(event->mimeData(), Qt::CopyAction, index.row(), index.column(), QModelIndex());
event->acceptProposedAction();
}
void ExtendedTableWidget::selectTableLine(int lineToSelect)
{
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
// Are there even that many lines?
if(lineToSelect >= m->rowCount())
return;
QApplication::setOverrideCursor( Qt::WaitCursor );
m->triggerCacheLoad(lineToSelect);
// Select it
clearSelection();
selectRow(lineToSelect);
scrollTo(currentIndex(), QAbstractItemView::PositionAtTop);
QApplication::restoreOverrideCursor();
}
void ExtendedTableWidget::selectTableLines(int firstLine, int count)
{
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
int lastLine = firstLine+count-1;
// Are there even that many lines?
if(lastLine >= m->rowCount())
return;
selectTableLine(firstLine);
QModelIndex topLeft = m->index(firstLine, 0);
QModelIndex bottomRight = m->index(lastLine, m->columnCount()-1);
selectionModel()->select(QItemSelection(topLeft, bottomRight), QItemSelectionModel::Select | QItemSelectionModel::Rows);
}
void ExtendedTableWidget::selectAll()
{
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
// Fetch all the data if needed and user accepts, then call parent's selectAll()
QMessageBox::StandardButton answer = QMessageBox::Yes;
// If we can fetch more data, ask user if they are sure about it.
if (!m->isCacheComplete()) {
answer = QMessageBox::question(this, QApplication::applicationName(),
tr("<p>Not all data has been loaded. <b>Do you want to load all data before selecting all the rows?</b><p>"
"<p>Answering <b>No</b> means that no more data will be loaded and the selection will not be performed.<br/>"
"Answering <b>Yes</b> might take some time while the data is loaded but the selection will be complete.</p>"
"Warning: Loading all the data might require a great amount of memory for big tables."),
QMessageBox::Yes | QMessageBox::No);
if (answer == QMessageBox::Yes)
m->completeCache();
}
if (answer == QMessageBox::Yes)
QTableView::selectAll();
}
void ExtendedTableWidget::openPrintDialog()
{
QMimeData *mimeData = new QMimeData;
QModelIndexList indices;
// Print the selection, if active, or the entire table otherwise.
// Given that simply clicking over a cell, selects it, one-cell selections are ignored.
if (selectionModel()->hasSelection() && selectionModel()->selectedIndexes().count() > 1)
indices = selectionModel()->selectedIndexes();
else
for (int row=0; row < model()->rowCount(); row++)
for (int column=0; column < model()->columnCount(); column++)
indices << model()->index(row, column);
// Copy the specified indices content to mimeData for getting the HTML representation of
// the table with headers. We can then print it using an HTML text document.
copyMimeData(indices, mimeData, true, false);
QPrinter printer;
QPrintPreviewDialog *dialog = new QPrintPreviewDialog(&printer);
connect(dialog, &QPrintPreviewDialog::paintRequested, this, [mimeData](QPrinter *previewPrinter) {
QTextDocument document;
document.setHtml(mimeData->html());
document.print(previewPrinter);
});
dialog->exec();
delete dialog;
delete mimeData;
}
void ExtendedTableWidget::sortByColumns(const std::vector<sqlb::OrderBy>& columns)
{
// Are there even any columns to sort by?
if(columns.size() == 0)
return;
// We only support sorting for SqliteTableModels with support for multiple and named sort columns
SqliteTableModel* sqlite_model = dynamic_cast<SqliteTableModel*>(model());
if(sqlite_model)
sqlite_model->sort(columns);
}
void ExtendedTableWidget::currentChanged(const QModelIndex &current, const QModelIndex &previous)
{
QTableView::currentChanged(current, previous);
emit currentIndexChanged(current, previous);
}
void ExtendedTableWidget::setToNull(const QModelIndexList& indices)
{
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
sqlb::TablePtr currentTable = m->db().getTableByName(m->currentTableName());
// Check if some column in the selection has a NOT NULL constraint, before trying to update the cells.
if(currentTable) {
for(const QModelIndex& index : indices) {
// Do not process rowid column
if(index.column() != 0) {
const size_t indexField = static_cast<size_t>(index.column()-1);
const sqlb::Field& field = currentTable->fields.at(indexField);
if(field.notnull()) {
QMessageBox::warning(nullptr, qApp->applicationName(),
tr("Cannot set selection to NULL. Column %1 has a NOT NULL constraint.").
arg(QString::fromStdString(field.name())));
return;
}
}
}
}
for(const QModelIndex& index : indices) {
// Update aborting in case of any error (to avoid repetitive errors like "Database is locked")
if(!model()->setData(index, QVariant()))
return;
}
}
void ExtendedTableWidget::setFrozenColumns(size_t count)
{
if(!m_frozen_table_view)
return;
m_frozen_column_count = count;
// Set up frozen table view widget
m_frozen_table_view->setModel(model());
m_frozen_table_view->setSelectionModel(selectionModel());
for(int i=0;i<model()->columnCount();i++)
m_frozen_table_view->setItemDelegateForColumn(i, nullptr);
m_frozen_table_view->setItemDelegateForColumn(static_cast<int>(m_frozen_column_count-1), m_item_border_delegate);
// Only show frozen columns in extra table view and copy column widths
m_frozen_table_view->horizontalHeader()->blockSignals(true); // Signals need to be blocked because hiding a column would emit resizedSection
for(size_t col=0;col<static_cast<size_t>(model()->columnCount());++col)
m_frozen_table_view->setColumnHidden(static_cast<int>(col), col >= count);
m_frozen_table_view->horizontalHeader()->blockSignals(false);
for(int col=0;col<static_cast<int>(count);++col)
m_frozen_table_view->setColumnWidth(col, columnWidth(col));
updateFrozenTableGeometry();
// Only show extra table view when there are frozen columns to see
if(count)
m_frozen_table_view->show();
else
m_frozen_table_view->hide();
}
void ExtendedTableWidget::generateFilters(size_t number, bool show_rowid)
{
m_tableHeader->generateFilters(number, m_frozen_column_count);
if(m_frozen_table_view)
{
size_t frozen_columns = std::min(m_frozen_column_count, number);
m_frozen_table_view->m_tableHeader->generateFilters(frozen_columns, show_rowid ? 0 : 1);
}
}
void ExtendedTableWidget::updateSectionWidth(int logicalIndex, int /* oldSize */, int newSize)
{
if(!m_frozen_table_view)
return;
if(logicalIndex < static_cast<int>(m_frozen_column_count))
{
m_frozen_table_view->setColumnWidth(logicalIndex, newSize);
setColumnWidth(logicalIndex, newSize);
updateFrozenTableGeometry();
}
}
void ExtendedTableWidget::updateSectionHeight(int logicalIndex, int /* oldSize */, int newSize)
{
if(!m_frozen_table_view)
return;
m_frozen_table_view->setRowHeight(logicalIndex, newSize);
}
void ExtendedTableWidget::resizeEvent(QResizeEvent* event)
{
QTableView::resizeEvent(event);
updateFrozenTableGeometry();
}
QModelIndex ExtendedTableWidget::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
{
QModelIndex current = QTableView::moveCursor(cursorAction, modifiers);
if(!m_frozen_table_view)
return current;
int width = 0;
for(int i=0;i<static_cast<int>(m_frozen_column_count);i++)
width += m_frozen_table_view->columnWidth(i);
if(cursorAction == MoveLeft && current.column() > 0 && visualRect(current).topLeft().x() < width)
{
const int newValue = horizontalScrollBar()->value() + visualRect(current).topLeft().x() - width;
horizontalScrollBar()->setValue(newValue);
}
return current;
}
void ExtendedTableWidget::scrollTo(const QModelIndex& index, ScrollHint hint)
{
if(index.column() >= static_cast<int>(m_frozen_column_count))
QTableView::scrollTo(index, hint);
}
void ExtendedTableWidget::updateFrozenTableGeometry()
{
if(!m_frozen_table_view)
return;
int width = 0;
for(int i=0;i<static_cast<int>(m_frozen_column_count);i++)
{
if(!isColumnHidden(i))
width += columnWidth(i);
}
m_frozen_table_view->setGeometry(verticalHeader()->width() + frameWidth(),
frameWidth(),
width,
viewport()->height() + horizontalHeader()->height());
}
void ExtendedTableWidget::setEditTriggers(QAbstractItemView::EditTriggers editTriggers)
{
QTableView::setEditTriggers(editTriggers);
if(m_frozen_table_view)
m_frozen_table_view->setEditTriggers(editTriggers);
}
void ExtendedTableWidget::setFilter(size_t column, const QString& value)
{
filterHeader()->setFilter(column, value);
if(m_frozen_table_view)
m_frozen_table_view->filterHeader()->setFilter(column, value);
}