mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-01-20 02:50:46 -06:00
483 lines
16 KiB
C++
483 lines
16 KiB
C++
#include "ExtendedTableWidget.h"
|
|
#include "sqlitetablemodel.h"
|
|
#include "FilterTableHeader.h"
|
|
#include "sqlitetypes.h"
|
|
#include "Settings.h"
|
|
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QKeySequence>
|
|
#include <QKeyEvent>
|
|
#include <QScrollBar>
|
|
#include <QHeaderView>
|
|
#include <QMessageBox>
|
|
#include <QBuffer>
|
|
#include <QMenu>
|
|
|
|
namespace
|
|
{
|
|
|
|
QList<QStringList> parseClipboard(const QString& clipboard)
|
|
{
|
|
QList<QStringList> result;
|
|
result.push_back(QStringList());
|
|
|
|
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);
|
|
result.last().push_back(text);
|
|
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.last().push_back(QString());
|
|
} else {
|
|
text = clipboard.mid(whitespace_offset, pos - whitespace_offset);
|
|
result.last().push_back(text);
|
|
}
|
|
|
|
if (ws.endsWith("\n"))
|
|
// create new row
|
|
result.push_back(QStringList());
|
|
|
|
whitespace_offset = offset = pos + ws.length();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
}
|
|
|
|
ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) :
|
|
QTableView(parent)
|
|
{
|
|
setHorizontalScrollMode(ExtendedTableWidget::ScrollPerPixel);
|
|
// Force ScrollPerItem, so scrolling shows all table rows
|
|
setVerticalScrollMode(ExtendedTableWidget::ScrollPerItem);
|
|
|
|
connect(verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(vscrollbarChanged(int)));
|
|
connect(this, SIGNAL(clicked(QModelIndex)), this, SLOT(cellClicked(QModelIndex)));
|
|
|
|
// Set up filter row
|
|
m_tableHeader = new FilterTableHeader(this);
|
|
setHorizontalHeader(m_tableHeader);
|
|
|
|
// Set up vertical header context menu
|
|
verticalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
// Set up table view context menu
|
|
m_contextMenu = new QMenu(this);
|
|
QAction* nullAction = new QAction(tr("Set to NULL"), m_contextMenu);
|
|
QAction* copyAction = new QAction(QIcon(":/icons/copy"), tr("Copy"), m_contextMenu);
|
|
QAction* pasteAction = new QAction(QIcon(":/icons/paste"), tr("Paste"), m_contextMenu);
|
|
QAction* filterAction = new QAction(tr("Use as Filter"), m_contextMenu);
|
|
m_contextMenu->addAction(filterAction);
|
|
m_contextMenu->addSeparator();
|
|
m_contextMenu->addAction(nullAction);
|
|
m_contextMenu->addSeparator();
|
|
m_contextMenu->addAction(copyAction);
|
|
m_contextMenu->addAction(pasteAction);
|
|
setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
// Set up context menu actions
|
|
connect(this, static_cast<void(QTableView::*)(const QPoint&)>(&QTableView::customContextMenuRequested),
|
|
[=](const QPoint& pos)
|
|
{
|
|
// 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(editable);
|
|
pasteAction->setEnabled(editable);
|
|
|
|
// Show menu
|
|
m_contextMenu->popup(viewport()->mapToGlobal(pos));
|
|
});
|
|
connect(filterAction, &QAction::triggered, [&]() {
|
|
useAsFilter();
|
|
});
|
|
connect(nullAction, &QAction::triggered, [&]() {
|
|
for(const QModelIndex& index : selectedIndexes())
|
|
model()->setData(index, QVariant());
|
|
});
|
|
connect(copyAction, &QAction::triggered, [&]() {
|
|
copy();
|
|
});
|
|
connect(pasteAction, &QAction::triggered, [&]() {
|
|
paste();
|
|
});
|
|
}
|
|
|
|
void ExtendedTableWidget::reloadSettings()
|
|
{
|
|
// Set the new font and font size
|
|
QFont dataBrowserFont(Settings::getValue("databrowser", "font").toString());
|
|
dataBrowserFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt());
|
|
setFont(dataBrowserFont);
|
|
|
|
// Set new default row height depending on the font size
|
|
verticalHeader()->setDefaultSectionSize(verticalHeader()->fontMetrics().height()+10);
|
|
}
|
|
|
|
void ExtendedTableWidget::copy()
|
|
{
|
|
QModelIndexList indices = selectionModel()->selectedIndexes();
|
|
|
|
// Abort if there's nothing to copy
|
|
if (indices.isEmpty())
|
|
return;
|
|
qSort(indices);
|
|
|
|
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
|
|
|
|
m_buffer.clear();
|
|
|
|
// If a single cell is selected, copy it to clipboard
|
|
if (indices.size() == 1) {
|
|
QImage img;
|
|
QVariant data = m->data(indices.first(), Qt::EditRole);
|
|
|
|
if (img.loadFromData(data.toByteArray())) { // If it's an image
|
|
qApp->clipboard()->setImage(img);
|
|
return;
|
|
} else {
|
|
QString text = data.toString();
|
|
if (text.isEmpty()) {
|
|
// NULL and empty single-cells are handled via inner buffer
|
|
qApp->clipboard()->clear();
|
|
QByteArrayList lst;
|
|
lst << data.toByteArray();
|
|
m_buffer.push_back(lst);
|
|
return;
|
|
}
|
|
|
|
if (text.contains('\n'))
|
|
text = QString("\"%1\"").arg(text);
|
|
qApp->clipboard()->setText(text);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If any of the cells contain binary data - we use inner buffer
|
|
bool containsBinary = false;
|
|
for(const QModelIndex& index : indices)
|
|
{
|
|
if (m->isBinary(index)) {
|
|
containsBinary = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (containsBinary) {
|
|
qApp->clipboard()->clear();
|
|
// Copy selected data into inner buffer
|
|
int columns = indices.last().column() - indices.first().column() + 1;
|
|
while (!indices.isEmpty()) {
|
|
QByteArrayList lst;
|
|
for (int i = 0; i < columns; ++i) {
|
|
lst << indices.first().data(Qt::EditRole).toByteArray();
|
|
indices.pop_front();
|
|
}
|
|
m_buffer.push_back(lst);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
QModelIndex first = indices.first();
|
|
QString result;
|
|
int currentRow = 0;
|
|
|
|
for(const QModelIndex& index : indices) {
|
|
if (first == index) { /* first index */ }
|
|
else if (index.row() != currentRow)
|
|
result.append("\r\n");
|
|
else
|
|
result.append("\t");
|
|
|
|
currentRow = index.row();
|
|
QVariant data = index.data(Qt::EditRole);
|
|
|
|
// non-NULL data is enquoted, whilst NULL isn't
|
|
if (!data.isNull()) {
|
|
QString text = data.toString();
|
|
text.replace("\"", "\"\"");
|
|
result.append(QString("\"%1\"").arg(text));
|
|
}
|
|
}
|
|
|
|
qApp->clipboard()->setText(result);
|
|
}
|
|
|
|
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());
|
|
|
|
// If clipboard contains image - just insert it
|
|
QImage img = qApp->clipboard()->image();
|
|
if (!img.isNull()) {
|
|
QByteArray ba;
|
|
QBuffer buffer(&ba);
|
|
buffer.open(QIODevice::WriteOnly);
|
|
img.save(&buffer, "PNG");
|
|
buffer.close();
|
|
|
|
m->setData(indices.first(), ba);
|
|
return;
|
|
}
|
|
|
|
QString clipboard = qApp->clipboard()->text();
|
|
|
|
if (clipboard.isEmpty() && !m_buffer.isEmpty()) {
|
|
// If buffer contains something - use it instead of clipboard
|
|
int rows = m_buffer.size();
|
|
int columns = m_buffer.first().size();
|
|
|
|
int firstRow = indices.front().row();
|
|
int firstColumn = indices.front().column();
|
|
|
|
int lastRow = qMin(firstRow + rows - 1, m->rowCount() - 1);
|
|
int lastColumn = qMin(firstColumn + columns - 1, m->columnCount() - 1);
|
|
|
|
int row = firstRow;
|
|
|
|
for(const QByteArrayList& lst : m_buffer) {
|
|
int column = firstColumn;
|
|
for(const QByteArray& ba : lst) {
|
|
m->setData(m->index(row, column), ba);
|
|
|
|
column++;
|
|
if (column > lastColumn)
|
|
break;
|
|
}
|
|
|
|
row++;
|
|
if (row > lastRow)
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
QList<QStringList> clipboardTable = parseClipboard(clipboard);
|
|
|
|
int clipboardRows = clipboardTable.size();
|
|
int clipboardColumns = clipboardTable.front().size();
|
|
|
|
// Sort the items by row, then by column
|
|
qSort(indices);
|
|
|
|
// Starting from assumption that selection is rectangular, and then first index is upper-left corner and last is lower-right.
|
|
int firstRow = indices.front().row();
|
|
int selectedRows = indices.back().row() - firstRow + 1;
|
|
int firstColumn = indices.front().column();
|
|
int selectedColumns = indices.back().column() - firstColumn + 1;
|
|
|
|
|
|
// If not selected only one cell then check does selection match cliboard dimensions
|
|
if(selectedRows != 1 || selectedColumns != 1)
|
|
{
|
|
if(selectedRows != clipboardRows || selectedColumns != clipboardColumns)
|
|
{
|
|
// Ask user is it sure about this
|
|
QMessageBox::StandardButton reply = 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);
|
|
if(reply != QMessageBox::Yes)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// Here we have positive answer even if cliboard is bigger than selection
|
|
|
|
|
|
// If last row and column are after table size clamp it
|
|
int lastRow = qMin(firstRow + clipboardRows - 1, m->rowCount() - 1);
|
|
int lastColumn = qMin(firstColumn + clipboardColumns - 1, m->columnCount() - 1);
|
|
|
|
int row = firstRow;
|
|
for(const QStringList& clipboardRow : clipboardTable)
|
|
{
|
|
int column = firstColumn;
|
|
for(const QString& cell : clipboardRow)
|
|
{
|
|
if (cell.isEmpty())
|
|
m->setData(m->index(row, column), QVariant());
|
|
else
|
|
{
|
|
QString text = cell;
|
|
if (QRegExp("\".*\"").exactMatch(text))
|
|
text = text.mid(1, cell.length() - 2);
|
|
text.replace("\"\"", "\"");
|
|
m->setData(m->index(row, column), text);
|
|
}
|
|
|
|
column++;
|
|
if(column> lastColumn)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
row++;
|
|
if(row > lastRow)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void ExtendedTableWidget::useAsFilter()
|
|
{
|
|
QModelIndex index = selectionModel()->currentIndex();
|
|
|
|
// Abort if there's nothing to filter
|
|
if (!index.isValid() || !selectionModel()->hasSelection())
|
|
return;
|
|
|
|
QVariant data = model()->data(index, Qt::EditRole);
|
|
|
|
if (data.isNull())
|
|
m_tableHeader->setFilter(index.column(), "=NULL");
|
|
else
|
|
m_tableHeader->setFilter(index.column(), "=" + data.toString());
|
|
|
|
}
|
|
|
|
void ExtendedTableWidget::keyPressEvent(QKeyEvent* event)
|
|
{
|
|
// Call a custom copy method when Ctrl-C is pressed
|
|
if(event->matches(QKeySequence::Copy))
|
|
{
|
|
copy();
|
|
return;
|
|
} else if(event->matches(QKeySequence::Paste)) {
|
|
// Call a custom paste method when Ctrl-P is pressed
|
|
paste();
|
|
} 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)) {
|
|
if(event->modifiers().testFlag(Qt::AltModifier))
|
|
{
|
|
// When pressing Alt+Delete set the value to NULL
|
|
for(const QModelIndex& index : selectedIndexes())
|
|
model()->setData(index, QVariant());
|
|
} else {
|
|
// When pressing Delete only set the value to empty string
|
|
for(const QModelIndex& index : selectedIndexes())
|
|
model()->setData(index, "");
|
|
}
|
|
} 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();
|
|
|
|
// 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->totalRowCount() - numVisibleRows() + 1);
|
|
}
|
|
}
|
|
|
|
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
|
|
if((value + numVisibleRows()) >= model()->rowCount() && model()->canFetchMore(QModelIndex()))
|
|
model()->fetchMore(QModelIndex());
|
|
}
|
|
|
|
int ExtendedTableWidget::numVisibleRows()
|
|
{
|
|
// 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 = rowAt(height()) == -1 ? model()->rowCount() : rowAt(height());
|
|
|
|
// Calculate the number of visible rows
|
|
return row_bottom - row_top;
|
|
}
|
|
|
|
QSet<int> ExtendedTableWidget::selectedCols()
|
|
{
|
|
QSet<int> selectedCols;
|
|
for(const QModelIndex & idx : selectedIndexes())
|
|
selectedCols.insert(idx.column());
|
|
return selectedCols;
|
|
}
|
|
|
|
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(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());
|
|
}
|
|
}
|
|
|
|
void ExtendedTableWidget::dragEnterEvent(QDragEnterEvent* event)
|
|
{
|
|
event->accept();
|
|
}
|
|
|
|
void ExtendedTableWidget::dragMoveEvent(QDragMoveEvent* event)
|
|
{
|
|
event->accept();
|
|
}
|
|
|
|
void ExtendedTableWidget::dropEvent(QDropEvent* event)
|
|
{
|
|
QModelIndex index = indexAt(event->pos());
|
|
model()->dropMimeData(event->mimeData(), Qt::CopyAction, index.row(), index.column(), QModelIndex());
|
|
event->acceptProposedAction();
|
|
}
|