mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-02-09 13:18:33 -06:00
Table Browser: Support extended selections
Support of extended selections (non-contiguous cell selections using Control+Click). The following changes were necessary: - Copy to clipboard iterates over rows and columns and leaves holes for non-rectangular selections as empty cells (HTML) or as NULL (SQL). Fix: Additionally the SQL copy uses NULL when the cell has NULL, not ''. - Shortcuts for column and row selection take into account non-contiguous cells and only select those columns/rows. - The legend in the status line counts correctly non-contiguous rows or columns. - Delete Record has been adjusted so only contiguous selected cells are removed in a single step. Fix: selecting line after removing has been deleted since the standard behaviour is giving better result. - ExtendedSelection has been enabled in TableBrowser, it was already enabled in Execute SQL table-widget. Possible improvements: pasting from the internal clipboard does not keep the layout of copied non-rectangular selections. See issues #1104 and #2638
This commit is contained in:
@@ -418,13 +418,13 @@ ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) :
|
||||
connect(selectColumnShortcut, &QShortcut::activated, this, [this]() {
|
||||
if(!hasFocus() || selectionModel()->selectedIndexes().isEmpty())
|
||||
return;
|
||||
selectionModel()->select(QItemSelection(selectionModel()->selectedIndexes().first(), selectionModel()->selectedIndexes().last()), QItemSelectionModel::Select | QItemSelectionModel::Columns);
|
||||
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(QItemSelection(selectionModel()->selectedIndexes().first(), selectionModel()->selectedIndexes().last()), QItemSelectionModel::Select | QItemSelectionModel::Rows);
|
||||
selectionModel()->select(selectionModel()->selection(), QItemSelectionModel::Select | QItemSelectionModel::Rows);
|
||||
});
|
||||
|
||||
// Set up frozen columns child widget
|
||||
@@ -573,6 +573,13 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime
|
||||
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";
|
||||
@@ -586,10 +593,11 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime
|
||||
// Table headers
|
||||
if (withHeaders || inSQL) {
|
||||
htmlResult.append("<tr><th>");
|
||||
int firstColumn = indices.front().column();
|
||||
for(int i = firstColumn; i <= indices.back().column(); i++) {
|
||||
QByteArray headerText = model()->headerData(i, Qt::Horizontal, Qt::EditRole).toByteArray();
|
||||
if (i != firstColumn) {
|
||||
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(", ");
|
||||
@@ -604,79 +612,89 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime
|
||||
sqlInsertStatement.append(") VALUES (");
|
||||
}
|
||||
|
||||
// Table data rows
|
||||
for(const QModelIndex& index : indices) {
|
||||
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());
|
||||
const QString style = QString("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);
|
||||
// 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) {
|
||||
|
||||
// Separators. For first cell, only opening table row tags must be added for the HTML and nothing for the text version.
|
||||
if (indices.first() == index) {
|
||||
htmlResult.append(QString("<tr><td style=\"%1\">").arg(style));
|
||||
sqlResult.append(sqlInsertStatement);
|
||||
} else if (index.row() != currentRow) {
|
||||
result.append(rowSepText);
|
||||
htmlResult.append(QString("</td></tr><tr><td style=\"%1\">").arg(style));
|
||||
sqlResult.append(");" + rowSepText + sqlInsertStatement);
|
||||
} else {
|
||||
result.append(fieldSepText);
|
||||
htmlResult.append(QString("</td><td style=\"%1\">").arg(style));
|
||||
sqlResult.append(", ");
|
||||
}
|
||||
currentRow = index.row();
|
||||
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);
|
||||
}
|
||||
|
||||
QImage img;
|
||||
QVariant bArrdata = index.data(Qt::EditRole);
|
||||
// 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(", ");
|
||||
}
|
||||
|
||||
// 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();
|
||||
currentRow = index.row();
|
||||
|
||||
QString imageBase64 = ba.toBase64();
|
||||
htmlResult.append("<img src=\"data:image/png;base64,");
|
||||
htmlResult.append(imageBase64);
|
||||
result.append(QString());
|
||||
htmlResult.append("\" alt=\"Image\">");
|
||||
} else {
|
||||
QByteArray text;
|
||||
if (!m->isBinary(index)) {
|
||||
text = bArrdata.toByteArray();
|
||||
QImage img;
|
||||
QVariant bArrdata = indices.contains(index) ? index.data(Qt::EditRole) : QVariant();
|
||||
|
||||
// Table cell data: text
|
||||
if (text.contains('\n') || text.contains('\t'))
|
||||
htmlResult.append("<pre>" + QString(text).toHtmlEscaped() + "</pre>");
|
||||
else
|
||||
htmlResult.append(QString(text).toHtmlEscaped());
|
||||
// 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();
|
||||
|
||||
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() + "'" );
|
||||
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 )
|
||||
@@ -1022,6 +1040,14 @@ std::unordered_set<size_t> ExtendedTableWidget::colsInSelection() const
|
||||
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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <unordered_set>
|
||||
#include <set>
|
||||
|
||||
#include "sql/Query.h"
|
||||
|
||||
@@ -60,8 +61,10 @@ public:
|
||||
public:
|
||||
// Get set of selected columns (all cells in column has to be selected)
|
||||
std::unordered_set<size_t> selectedCols() const;
|
||||
// Get set of columns traversed by selection (only some cells in column has to be selected)
|
||||
// Get set of columns traversed by selection (only some cells in column have to be selected)
|
||||
std::unordered_set<size_t> colsInSelection() const;
|
||||
// Get set of ordered rows traversed by selection (only some cells in row have to be selected)
|
||||
std::set<size_t> rowsInSelection() const;
|
||||
|
||||
int numVisibleRows() const;
|
||||
|
||||
|
||||
@@ -474,9 +474,9 @@ void TableBrowser::refresh()
|
||||
const QModelIndexList& sel = ui->dataTable->selectionModel()->selectedIndexes();
|
||||
QString statusMessage;
|
||||
if (sel.count() > 1) {
|
||||
int rows = sel.last().row() - sel.first().row() + 1;
|
||||
int rows = static_cast<int>(ui->dataTable->rowsInSelection().size());
|
||||
statusMessage = tr("%n row(s)", "", rows);
|
||||
int columns = sel.last().column() - sel.first().column() + 1;
|
||||
int columns = static_cast<int>(ui->dataTable->colsInSelection().size());
|
||||
statusMessage += tr(", %n column(s)", "", columns);
|
||||
|
||||
if (sel.count() < Settings::getValue("databrowser", "complete_threshold").toInt()) {
|
||||
@@ -1075,8 +1075,6 @@ void TableBrowser::updateInsertDeleteRecordButton()
|
||||
{
|
||||
// Update the delete record button to reflect number of selected records
|
||||
|
||||
// NOTE: We're assuming here that the selection is always contiguous, i.e. that there are never two selected
|
||||
// rows with a non-selected row in between.
|
||||
int rows = 0;
|
||||
|
||||
// If there is no model yet (because e.g. no database file is opened) there is no selection model either. So we need to check for that here
|
||||
@@ -1303,22 +1301,29 @@ void TableBrowser::deleteRecord()
|
||||
if(ui->dataTable->selectionModel()->selectedIndexes().isEmpty())
|
||||
return;
|
||||
|
||||
int old_row = ui->dataTable->currentIndex().row();
|
||||
while(ui->dataTable->selectionModel()->hasSelection())
|
||||
{
|
||||
int first_selected_row = ui->dataTable->selectionModel()->selectedIndexes().first().row();
|
||||
int last_selected_row = ui->dataTable->selectionModel()->selectedIndexes().last().row();
|
||||
int selected_rows_count = last_selected_row - first_selected_row + 1;
|
||||
if(!m_model->removeRows(first_selected_row, selected_rows_count))
|
||||
std::set<size_t> row_set = ui->dataTable->rowsInSelection();
|
||||
int first_selected_row = static_cast<int>(*row_set.begin());
|
||||
int rows_to_remove = 0;
|
||||
int previous_row = first_selected_row - 1;
|
||||
|
||||
// Non-contiguous selection: remove only the contiguous
|
||||
// rows in the selection in each cycle until the entire
|
||||
// selection has been removed.
|
||||
for(size_t row : row_set) {
|
||||
if(previous_row == static_cast<int>(row - 1))
|
||||
rows_to_remove++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if(!m_model->removeRows(first_selected_row, rows_to_remove))
|
||||
{
|
||||
QMessageBox::warning(this, QApplication::applicationName(), tr("Error deleting record:\n%1").arg(db->lastError()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(old_row > m_model->rowCount())
|
||||
old_row = m_model->rowCount();
|
||||
selectTableLine(old_row);
|
||||
} else {
|
||||
QMessageBox::information( this, QApplication::applicationName(), tr("Please select a record first"));
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
<enum>Qt::CopyAction</enum>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ContiguousSelection</enum>
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
Reference in New Issue
Block a user