Skip to content

Commit c5199f5

Browse files
committed
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
1 parent 93a5c2d commit c5199f5

4 files changed

Lines changed: 123 additions & 89 deletions

File tree

src/ExtendedTableWidget.cpp

Lines changed: 100 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,13 @@ ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) :
418418
connect(selectColumnShortcut, &QShortcut::activated, this, [this]() {
419419
if(!hasFocus() || selectionModel()->selectedIndexes().isEmpty())
420420
return;
421-
selectionModel()->select(QItemSelection(selectionModel()->selectedIndexes().first(), selectionModel()->selectedIndexes().last()), QItemSelectionModel::Select | QItemSelectionModel::Columns);
421+
selectionModel()->select(selectionModel()->selection(), QItemSelectionModel::Select | QItemSelectionModel::Columns);
422422
});
423423
QShortcut* selectRowShortcut = new QShortcut(QKeySequence("Shift+Space"), this);
424424
connect(selectRowShortcut, &QShortcut::activated, this, [this]() {
425425
if(!hasFocus() || selectionModel()->selectedIndexes().isEmpty())
426426
return;
427-
selectionModel()->select(QItemSelection(selectionModel()->selectedIndexes().first(), selectionModel()->selectedIndexes().last()), QItemSelectionModel::Select | QItemSelectionModel::Rows);
427+
selectionModel()->select(selectionModel()->selection(), QItemSelectionModel::Select | QItemSelectionModel::Rows);
428428
});
429429

430430
// Set up frozen columns child widget
@@ -573,6 +573,13 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime
573573
htmlResult.append("<style type=\"text/css\">br{mso-data-placement:same-cell;}</style></head><body>"
574574
"<table border=1 cellspacing=0 cellpadding=2>");
575575

576+
// Insert the columns in a set, since they could be non-contiguous.
577+
std::set<int> colsInIndexes, rowsInIndexes;
578+
for(const QModelIndex & idx : indices) {
579+
colsInIndexes.insert(idx.column());
580+
rowsInIndexes.insert(idx.row());
581+
}
582+
576583
int currentRow = indices.first().row();
577584

578585
const QString fieldSepText = "\t";
@@ -586,10 +593,11 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime
586593
// Table headers
587594
if (withHeaders || inSQL) {
588595
htmlResult.append("<tr><th>");
589-
int firstColumn = indices.front().column();
590-
for(int i = firstColumn; i <= indices.back().column(); i++) {
591-
QByteArray headerText = model()->headerData(i, Qt::Horizontal, Qt::EditRole).toByteArray();
592-
if (i != firstColumn) {
596+
int firstColumn = *colsInIndexes.begin();
597+
598+
for(int col : colsInIndexes) {
599+
QByteArray headerText = model()->headerData(col, Qt::Horizontal, Qt::EditRole).toByteArray();
600+
if (col != firstColumn) {
593601
result.append(fieldSepText);
594602
htmlResult.append("</th><th>");
595603
sqlInsertStatement.append(", ");
@@ -604,79 +612,89 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime
604612
sqlInsertStatement.append(") VALUES (");
605613
}
606614

607-
// Table data rows
608-
for(const QModelIndex& index : indices) {
609-
QFont font;
610-
font.fromString(index.data(Qt::FontRole).toString());
611-
const QString fontStyle(font.italic() ? "italic" : "normal");
612-
const QString fontWeigth(font.bold() ? "bold" : "normal");
613-
const QString fontDecoration(font.underline() ? " text-decoration: underline;" : "");
614-
const QColor bgColor(index.data(Qt::BackgroundRole).toString());
615-
const QColor fgColor(index.data(Qt::ForegroundRole).toString());
616-
const Qt::Alignment align(index.data(Qt::TextAlignmentRole).toInt());
617-
const QString textAlign(CondFormat::alignmentTexts().at(CondFormat::fromCombinedAlignment(align)).toLower());
618-
const QString style = QString("font-family: '%1'; font-size: %2pt; font-style: %3; font-weight: %4;%5 "
619-
"background-color: %6; color: %7; text-align: %8").arg(
620-
font.family().toHtmlEscaped(),
621-
QString::number(font.pointSize()),
622-
fontStyle,
623-
fontWeigth,
624-
fontDecoration,
625-
bgColor.name(),
626-
fgColor.name(),
627-
textAlign);
628-
629-
// Separators. For first cell, only opening table row tags must be added for the HTML and nothing for the text version.
630-
if (indices.first() == index) {
631-
htmlResult.append(QString("<tr><td style=\"%1\">").arg(style));
632-
sqlResult.append(sqlInsertStatement);
633-
} else if (index.row() != currentRow) {
634-
result.append(rowSepText);
635-
htmlResult.append(QString("</td></tr><tr><td style=\"%1\">").arg(style));
636-
sqlResult.append(");" + rowSepText + sqlInsertStatement);
637-
} else {
638-
result.append(fieldSepText);
639-
htmlResult.append(QString("</td><td style=\"%1\">").arg(style));
640-
sqlResult.append(", ");
641-
}
642-
currentRow = index.row();
615+
// Iterate over rows x cols checking if the index actually exists when needed, in order
616+
// to support non-rectangular selections.
617+
for(const int row : rowsInIndexes) {
618+
for(const int column : colsInIndexes) {
619+
620+
const QModelIndex index = indices.first().sibling(row, column);
621+
QString style;
622+
if(indices.contains(index)) {
623+
QFont font;
624+
font.fromString(index.data(Qt::FontRole).toString());
625+
const QString fontStyle(font.italic() ? "italic" : "normal");
626+
const QString fontWeigth(font.bold() ? "bold" : "normal");
627+
const QString fontDecoration(font.underline() ? " text-decoration: underline;" : "");
628+
const QColor bgColor(index.data(Qt::BackgroundRole).toString());
629+
const QColor fgColor(index.data(Qt::ForegroundRole).toString());
630+
const Qt::Alignment align(index.data(Qt::TextAlignmentRole).toInt());
631+
const QString textAlign(CondFormat::alignmentTexts().at(CondFormat::fromCombinedAlignment(align)).toLower());
632+
style = QString("style=\"font-family: '%1'; font-size: %2pt; font-style: %3; font-weight: %4;%5 "
633+
"background-color: %6; color: %7; text-align: %8\"").arg(
634+
font.family().toHtmlEscaped(),
635+
QString::number(font.pointSize()),
636+
fontStyle,
637+
fontWeigth,
638+
fontDecoration,
639+
bgColor.name(),
640+
fgColor.name(),
641+
textAlign);
642+
}
643643

644-
QImage img;
645-
QVariant bArrdata = index.data(Qt::EditRole);
644+
// Separators. For first cell, only opening table row tags must be added for the HTML and nothing for the text version.
645+
if (index.row() == *rowsInIndexes.begin() && index.column() == *colsInIndexes.begin()) {
646+
htmlResult.append(QString("<tr><td %1>").arg(style));
647+
sqlResult.append(sqlInsertStatement);
648+
} else if (index.row() != currentRow) {
649+
result.append(rowSepText);
650+
htmlResult.append(QString("</td></tr><tr><td %1>").arg(style));
651+
sqlResult.append(");" + rowSepText + sqlInsertStatement);
652+
} else {
653+
result.append(fieldSepText);
654+
htmlResult.append(QString("</td><td %1>").arg(style));
655+
sqlResult.append(", ");
656+
}
646657

647-
// Table cell data: image? Store it as an embedded image in HTML
648-
if (!inSQL && img.loadFromData(bArrdata.toByteArray()))
649-
{
650-
QByteArray ba;
651-
QBuffer buffer(&ba);
652-
buffer.open(QIODevice::WriteOnly);
653-
img.save(&buffer, "PNG");
654-
buffer.close();
655-
656-
QString imageBase64 = ba.toBase64();
657-
htmlResult.append("<img src=\"data:image/png;base64,");
658-
htmlResult.append(imageBase64);
659-
result.append(QString());
660-
htmlResult.append("\" alt=\"Image\">");
661-
} else {
662-
QByteArray text;
663-
if (!m->isBinary(index)) {
664-
text = bArrdata.toByteArray();
665-
666-
// Table cell data: text
667-
if (text.contains('\n') || text.contains('\t'))
668-
htmlResult.append("<pre>" + QString(text).toHtmlEscaped() + "</pre>");
669-
else
670-
htmlResult.append(QString(text).toHtmlEscaped());
671-
672-
result.append(text);
673-
sqlResult.append(sqlb::escapeString(text));
674-
} else
675-
// Table cell data: binary. Save as BLOB literal in SQL
676-
sqlResult.append( "X'" + bArrdata.toByteArray().toHex() + "'" );
658+
currentRow = index.row();
659+
660+
QImage img;
661+
QVariant bArrdata = indices.contains(index) ? index.data(Qt::EditRole) : QVariant();
677662

663+
// Table cell data: image? Store it as an embedded image in HTML
664+
if (!inSQL && img.loadFromData(bArrdata.toByteArray()))
665+
{
666+
QByteArray ba;
667+
QBuffer buffer(&ba);
668+
buffer.open(QIODevice::WriteOnly);
669+
img.save(&buffer, "PNG");
670+
buffer.close();
671+
672+
QString imageBase64 = ba.toBase64();
673+
htmlResult.append("<img src=\"data:image/png;base64,");
674+
htmlResult.append(imageBase64);
675+
result.append(QString());
676+
htmlResult.append("\" alt=\"Image\">");
677+
} else {
678+
if (bArrdata.isNull()) {
679+
sqlResult.append("NULL");
680+
} else if(!m->isBinary(index)) {
681+
QByteArray text = bArrdata.toByteArray();
682+
683+
// Table cell data: text
684+
if (text.contains('\n') || text.contains('\t'))
685+
htmlResult.append("<pre>" + QString(text).toHtmlEscaped() + "</pre>");
686+
else
687+
htmlResult.append(QString(text).toHtmlEscaped());
688+
689+
result.append(text);
690+
sqlResult.append(sqlb::escapeString(text));
691+
} else
692+
// Table cell data: binary. Save as BLOB literal in SQL
693+
sqlResult.append( "X'" + bArrdata.toByteArray().toHex() + "'" );
694+
}
678695
}
679696
}
697+
680698
sqlResult.append(");");
681699

682700
if ( inSQL )
@@ -1022,6 +1040,14 @@ std::unordered_set<size_t> ExtendedTableWidget::colsInSelection() const
10221040
return colsInSelection;
10231041
}
10241042

1043+
std::set<size_t> ExtendedTableWidget::rowsInSelection() const
1044+
{
1045+
std::set<size_t> rowsInSelection;
1046+
for(const QModelIndex & idx : selectedIndexes())
1047+
rowsInSelection.insert(static_cast<size_t>(idx.row()));
1048+
return rowsInSelection;
1049+
}
1050+
10251051
void ExtendedTableWidget::cellClicked(const QModelIndex& index)
10261052
{
10271053
// If Ctrl-Shift is pressed try to jump to the row referenced by the foreign key of the clicked cell

src/ExtendedTableWidget.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <QStyledItemDelegate>
66
#include <QSortFilterProxyModel>
77
#include <unordered_set>
8+
#include <set>
89

910
#include "sql/Query.h"
1011

@@ -60,8 +61,10 @@ class ExtendedTableWidget : public QTableView
6061
public:
6162
// Get set of selected columns (all cells in column has to be selected)
6263
std::unordered_set<size_t> selectedCols() const;
63-
// Get set of columns traversed by selection (only some cells in column has to be selected)
64+
// Get set of columns traversed by selection (only some cells in column have to be selected)
6465
std::unordered_set<size_t> colsInSelection() const;
66+
// Get set of ordered rows traversed by selection (only some cells in row have to be selected)
67+
std::set<size_t> rowsInSelection() const;
6568

6669
int numVisibleRows() const;
6770

src/TableBrowser.cpp

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,9 @@ void TableBrowser::refresh()
474474
const QModelIndexList& sel = ui->dataTable->selectionModel()->selectedIndexes();
475475
QString statusMessage;
476476
if (sel.count() > 1) {
477-
int rows = sel.last().row() - sel.first().row() + 1;
477+
int rows = static_cast<int>(ui->dataTable->rowsInSelection().size());
478478
statusMessage = tr("%n row(s)", "", rows);
479-
int columns = sel.last().column() - sel.first().column() + 1;
479+
int columns = static_cast<int>(ui->dataTable->colsInSelection().size());
480480
statusMessage += tr(", %n column(s)", "", columns);
481481

482482
if (sel.count() < Settings::getValue("databrowser", "complete_threshold").toInt()) {
@@ -1075,8 +1075,6 @@ void TableBrowser::updateInsertDeleteRecordButton()
10751075
{
10761076
// Update the delete record button to reflect number of selected records
10771077

1078-
// NOTE: We're assuming here that the selection is always contiguous, i.e. that there are never two selected
1079-
// rows with a non-selected row in between.
10801078
int rows = 0;
10811079

10821080
// 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()
13031301
if(ui->dataTable->selectionModel()->selectedIndexes().isEmpty())
13041302
return;
13051303

1306-
int old_row = ui->dataTable->currentIndex().row();
13071304
while(ui->dataTable->selectionModel()->hasSelection())
13081305
{
1309-
int first_selected_row = ui->dataTable->selectionModel()->selectedIndexes().first().row();
1310-
int last_selected_row = ui->dataTable->selectionModel()->selectedIndexes().last().row();
1311-
int selected_rows_count = last_selected_row - first_selected_row + 1;
1312-
if(!m_model->removeRows(first_selected_row, selected_rows_count))
1306+
std::set<size_t> row_set = ui->dataTable->rowsInSelection();
1307+
int first_selected_row = static_cast<int>(*row_set.begin());
1308+
int rows_to_remove = 0;
1309+
int previous_row = first_selected_row - 1;
1310+
1311+
// Non-contiguous selection: remove only the contiguous
1312+
// rows in the selection in each cycle until the entire
1313+
// selection has been removed.
1314+
for(size_t row : row_set) {
1315+
if(previous_row == static_cast<int>(row - 1))
1316+
rows_to_remove++;
1317+
else
1318+
break;
1319+
}
1320+
1321+
if(!m_model->removeRows(first_selected_row, rows_to_remove))
13131322
{
13141323
QMessageBox::warning(this, QApplication::applicationName(), tr("Error deleting record:\n%1").arg(db->lastError()));
13151324
break;
13161325
}
13171326
}
1318-
1319-
if(old_row > m_model->rowCount())
1320-
old_row = m_model->rowCount();
1321-
selectTableLine(old_row);
13221327
} else {
13231328
QMessageBox::information( this, QApplication::applicationName(), tr("Please select a record first"));
13241329
}

src/TableBrowser.ui

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192
<enum>Qt::CopyAction</enum>
193193
</property>
194194
<property name="selectionMode">
195-
<enum>QAbstractItemView::ContiguousSelection</enum>
195+
<enum>QAbstractItemView::ExtendedSelection</enum>
196196
</property>
197197
</widget>
198198
</item>

0 commit comments

Comments
 (0)